Peridot In Action

Peridot In The Grand Scheme Of Things

By Brian Scaturro / @scaturr

Code samples extracted from Behavior Driven Todos

What is BDD?

BDD = Behavior Driven Development

What do we mean by behavior?

  1. That stuff users do.
  2. The thing your application does.
  3. What code does.

The BDD Lifecycle

(Start with Behat)

1

Focus on one scenario

2

Write a failing step definition

(Drop down to Peridot)

3

Write a failing spec

4

Get the spec to pass

5

Refactor

(When the step is passing)

7

Refactor

Why it matters

Focus on the how, not the what

  • How should the application function?
  • How do objects and functions interact?
  • Did we meet our application/code expectations?
Peridot

highly extensible, highly enjoyable, BDD testing framework for PHP

Hello Peridot!

							
<?php
describe('ArrayObject', function () {
    beforeEach(function () {
        $this->arrayObject = new ArrayObject(['one', 'two', 'three']);
    });

    describe('->count()', function () {
        it('should return the number of items', function () {
            $count = $this->arrayObject->count();
            assert($count === 3, 'expected 3');
        });
    });
});
							
						

Peridot's Role

An easy to use BDD/TDD tool. Useful for testing units, libraries, and apps!

Behat

A BDD framework for PHP to help you test business expectations.

Hello Behat

Features

							
Feature: toggling the status of a todo

As a user
I want to be able to mark todos complete

Scenario: viewing a done todo
  Given I have a done todo "Get groceries"
  When I am on "/"
  Then I should see 1 ".todo-complete" element after waiting
							
						

Contexts

							
<?php
class FeatureContext
{
    /**
     * @Given I have a done todo :arg1
     */
    public function iHaveADoneTodo($todoText)
    {
        $collection = self::getTodoCollection();
        $collection->insert(['label' => $todoText, 'done' => true]);
    }

    /**
     * Opens specified page.
     *
     * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/
     * @When /^(?:|I )go to "(?P<page>[^"]+)"$/
     */
    public function visit($page)
    {
        $this->visitPath($page);
    }

    /**
     * @Then I should see :arg2 :arg1 element(s) after waiting
     */
    public function iShouldSeeElementAfterWaiting($number, $selector)
    {
        $this->getSession()->wait(10000, "document.querySelectorAll('$selector').length === $number");
        $this->assertNumElements($number, $selector);
    }
}
							
						

Behat's Role

Great for acceptance tests, planning, and testing business expectations.

Mink

Browser emulation for PHP. The Mink extension for Behat is great for automating application acceptance tests that use Selenium WebDriver.

Peridot + Behat = BDD Nirvana

Planning

User Stories

MUST!

  • Have business value
  • Be testable
  • Be small enough to implement in one iteration

Acceptance test-driven planning with Behat

In an ideal world, we gather around this acceptance criteria during planning and write testable acceptance criteria with stakeholders.

We can translate our user stories into automated acceptance tests with Behat!

BDD Lifecycle In Action

A Failing Scenario

							
Feature: initial visit to the todo app

As a user
I want to input todos
So I can track my business

Scenario: visiting todos for the first time
  Given I am on "/"
  Then I should see "Todos"
  And I should see a "#todo" element
							
						
failing scenario

Get to green!

							
<!-- public/index.php -->
<h1>Todos<h1>
<input type="text" id="todo" />
							
						
passing scenario

Repeat!

							
Feature: adding a todo

As a user
I want my todos to be persisted
So I don't have to retype them

Scenario: adding a todo
  Given I am on "/"
  When I fill in "todo" with "Get groceries"
  And I press "add"
  And I reload the page
  Then I should see "Get groceries"
							
						
failing add todo scenario

We can pass that step!

							
<!-- public/index.php -->
<h1>Todos<h1>
<input type="text" id="todo" /> <button id="add">add</button>
							
						
still failing scenario

Getting Serious

We've started our acceptance test cadence. However - we need to come to terms with reality: A single index.php probably won't cut it. We need to start writing real code.

The decision has been made to use a framework for the code behind the features. We happen to like Silex.

Drop to Peridot

We need to start writing code to support the features of our todo application. We are at a point where we want to pursue BDD at the unit level.

							
<?php
use Symfony\Component\HttpFoundation\Request;

describe('TodosController', function () {

    beforeEach(function () {
        $this->controller = new TodosController();
    });

    describe('->create()', function () {

        it('should insert a todo', function () {
            $request = Request::create('/todos', 'POST', [], [], [], [], json_encode([
                'label' => 'Get groceries'
            ]));

            $this->controller->create($request);
            // what is our behavior?
        });
    });
});
							
						

Peridot And Prophecy

Mocks and spies are a great way to verify behavior. Peridot has a plugin for using the amazing mocking framework: Prophecy

Using Plugins

Plugins are installed like any other package, and are included via the peridot.php file contained in the project root.

Registering the Prophecy plugin

							
<?php //peridot.php
use Peridot\Plugin\Prophecy\ProphecyPlugin;

// if peridot.php returns a closure it will be called with
// the Peridot event emitter, a key component to extending Peridot
return function($emitter) {
    $prophecy = new ProphecyPlugin($emitter);
};
							
						

This plugin adds Prophecy functionality to our specs.

Testing Behavior With Mocks

							
<?php
use Symfony\Component\HttpFoundation\Request;
use Brianium\Todos\Controller\TodosController;

describe('TodosController', function () {

    beforeEach(function () {
        $this->collection = $this->getProphet()->prophesize('MongoCollection');
        $this->controller = new TodosController($this->collection->reveal());
    });

    describe('->create()', function () {

        beforeEach(function () {
            $this->data = ['label' => 'Get groceries'];
        });

        it('should insert a todo', function () {
            $request = Request::create('/todos', 'POST', [], [], [], [], json_encode([
                'label' => 'Get groceries'
            ]));

            $this->controller->create($request);

            $this->collection->insert($this->data)->shouldBeCalled(); //thanks Prophecy!
            $this->getProphet()->checkPredictions();
        });
    });
});
							
						
failing peridot spec

Fix one thing at a time

							
<?php
namespace Brianium\Todos\Controller;

class TodosController
{

}
							
						
another failing peridot spec

Keeping making fixes

							
<?php
namespace Brianium\Todos\Controller;

class TodosController
{
    public function create()
    {

    }
}
							
						
yet another failing peridot spec

Bring it on home

							
<?php
namespace Brianium\Todos\Controller;

use MongoCollection;
use Symfony\Component\HttpFoundation\Request;

class TodosController
{
    private $todos;

    public function __construct(MongoCollection $todos)
    {
        $this->todos = $todos;
    }

    public function create(Request $request)
    {
        $payload = json_decode($request->getContent(), true);
        $this->todos->insert($payload);
    }
}
							
						
passing peridot spec

To make our API useful, we should return our new resource

							
<?php
use Symfony\Component\HttpFoundation\Request;
use Brianium\Todos\Controller\TodosController;

describe('TodosController', function () {

    beforeEach(function () {
        $this->collection = $this->getProphet()->prophesize('MongoCollection');
        $this->controller = new TodosController($this->collection->reveal());
    });

    describe('->create()', function () {

        beforeEach(function () {
            $this->data = ['label' => 'Get groceries'];
            $this->request = Request::create('/todos', 'POST', [], [], [], [], json_encode($this->data));
        });

        it('should insert a todo', function () {
            $this->controller->create($this->request);

            $this->collection->insert($this->data)->shouldBeCalled();
            $this->getProphet()->checkPredictions();
        });

        it('should return the new todo', function () {
            $response = $this->controller->create($this->request);

            $todo = json_decode($response->getContent());

            assert($todo->label == 'Get groceries');
        });
    });
});
							
						
no response returned

Returning the newly created resource

							
<?php
namespace Brianium\Todos\Controller;

use MongoCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class TodosController
{
    private $todos;

    public function __construct(MongoCollection $todos)
    {
        $this->todos = $todos;
    }

    public function create(Request $request)
    {
        $payload = json_decode($request->getContent(), true);
        $this->todos->insert($payload);
        return JsonResponse::create($payload);
    }
}
							
						
second passing peridot spec

Finishing Our Feature

using twig to render our todos

							
<html>
  <head>
    <title>Todo List</title>
  </head>
  <body>
    <h1>Todos</h1>
    <input type="text" id="todo" /> <button id="add">add</button>
    <ul id="todos">
      {% for todo in todos %}
      <li>{{ todo.label }}</li>
      {% endfor %}
    </ul>

    <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
    <script src="//code.jquery.com/jquery-2.1.3.min.js"></script>
    <script src="js/todos.js"></script>
  </body>
</html>
							
						

A controller action for index

							
<?php
namespace Brianium\Todos\Controller;

use MongoCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Twig_Environment;

class TodosController
{
    private $todos;
    private $twig;

    public function __construct(MongoCollection $todos, Twig_Environment $twig)
    {
        $this->todos = $todos;
        $this->twig = $twig;
    }

    public function index()
    {
        $content = $this->twig->render('index.twig', ['todos' => $this->todos->find()]);
        return Response::create($content);
    }

    public function create(Request $request)
    {
        $payload = json_decode($request->getContent(), true);
        $this->todos->insert($payload);
        return JsonResponse::create($payload);
    }
}
							
						

A Sprinkle Of JavaScript

							
(function () {

  var todo = $('#todo'),
      add = $('#add'),
      list = $('#todos');

  /**
   * Returns an event handler that appends
   * text from input to a list element.
   *
   * @param {jQuery} list
   * @param {jQuery} input
   * @return {Function}
   */
  function append(list, input) {
    return function () {
      var val = input.val();
      list.append('<li>' + val + '</li>');
      return val;
    };
  }

  /**
   * Persist the todo
   *
   * @param {String} val the todo label
   */
  function create(val) {
    $.ajax({
      method: 'POST',
      url: '/todos',
      data: JSON.stringify({label: val}),
      contentType: 'application/json',
      dataType: 'json'
    });
  }

  add.click(_.compose(create, append(list, todo)));

})();
							
						

Our Passing Feature

finished todo

Reference: Silex App File

How Silex works is beyond the scope of this talk, but if you want to refer to the todo app to see how a controller is tied to a route, take a look at the app file.

Iterate! Iterate! Iterate!

Sidebar: Peridot for route tests

A lot of behavior, especially for APIs, ends up outside of the context of a controller - usually in the form of middleware. This is a good thing, especially when it is repeated everywhere.

The HttpKernel Plugin

							
<?php
use Peridot\Plugin\Prophecy\ProphecyPlugin;
use Peridot\Plugin\HttpKernel\HttpKernelPlugin;

return function($emitter) {
    $prophecy = new ProphecyPlugin($emitter);

    //set up app tests
    $app = include __DIR__ . '/app/app.php';
    $app['debug'] = true;
    $app['exception_handler']->disable();
    $app['todos-collection'] = $app['mongo-client']->test->todos;
    HttpKernelPlugin::register($emitter, $app);
};
							
						

Testing high level API behavior

							
<?php
describe('/todos/{id}', function () {

    beforeEach(function () {
        $app = $this->getHttpKernelApplication();
        $app['todos-collection']->remove();

        $this->document = ['label' => 'Do a thing!'];
        $app['todos-collection']->insert($this->document);
    });

    describe('POST', function () {

        it('should return an error response if the todo already exists', function () {
            $this->client->request('POST', '/todos', [], [], [], '{"label":"Do a thing!"}');

            $response = $this->client->getResponse();

            assert($response->getStatusCode() === 422);
        });

    });
});
							
						

Questions?

- Full Todo App
- Peridot On GitHub