quarta-feira, 7 de setembro de 2011

Além de TDD com PHPSpec

Esta é a segunda parte de uma série sobre Fora-in Development Comportamento Impulsionada em PHP. A primeira parte introduz de fora para dentro, desenvolvimento e como executar cenários com Behat. Leia este para apanhar com as ferramentas eo exemplo que temos utilizado até agora, e depois voltar para descobrir como PHPSpec se encaixa nesse quadro.



PHPSpec é o primeiro framework PHP BDD. É um porto de RSpec para PHP criado em 2007 por Pádraic Brady e Swicegood Travis. Desenvolvimento, neste quadro parado por um tempo e foi reacendido agosto do ano passado (2010). PHPSpec pode ser instalado via pear, usando estes comandos:



$ sudo pear config-set preferred_state beta
$ sudo pear channel-discover pear.phpspec.net
$ sudo pear install  - all-deps phpspec / PHPSpec
Se você está acostumado a testes unitários, aqui é uma folha de tradução rápida para xUnit / xSpec termos:
  • Em xUnit nós teste , em xSpec que descrevem , de modo os seus nomes de classe começam com "Descrever"
  • Em PHPSpec, o arquivo spec para uma classe MyClass é nomeado MyClassSpec.php
  • Em testes xUnit nós grupo em um TestCase . Em xSpec temos exemplos que são agrupados em Contextos
  • Em xUnit cada método é um teste com o prefixo de teste . Em xSpec cada método é um exemplo , usamos o prefixo que
  • Em xUnit que afirmam que o trabalho algo tão esperado. Em xSpec que especificar como ele deve trabalhar
Nós usamos Gherkin e Behat para especificar como a nossa aplicação é suposto para trabalhar. Usamos PHPSpec para especificar o comportamento de nossas aulas. Para começar, vamos ver um exemplo simples mostrando isso. Estamos construindo uma classe que cumprimenta o usuário com "Olá, Mundo!". GreeterSpec.php arquivo ficaria assim:
class DescribeGreeter extends \PHPSpec\Context
{
    function before()
    {
        $this->greeter = $this->spec(new Greeter);
    }
 
    function itGreetsUsingAHelloWorldMessage()
    {
        $message = $this->greeter->greet();
        $message->should->be('Hello, World!');
    }
}
If you already have experience with Unit Testing, the above would look familiar to you. Note that instead of saying: $this->assertEquals('Hello, World!', $message) we say $message->should->be('Hello, World!'). We are describing how we want the behaviour to be, rather than testing it. The before() method is a setup method that would get run before any example is run; the spec() method is a decorator that wraps the object being tested so we call the expectations on the properties and methods results.
To run the spec we use the phpspec command:
$ phpspec GreeterSpec.php
First we run it and watch it fail. Then we write a Greeter class to satisfy the example.

PHPSpec and MVC

Going back to our Video Renting application. The newly released version (1.2beta) includes integration with Zend Framework which enables us to test the MVC (Model, View, Controller) components individually. When we test an MVC application, we should start with the view, because that’s what our scenario describes:
The text “Revolution OS” was not found anywhere in the text of the current page
Let’s keep all our specs in a folder called “spec”. To write a view spec we first add a folder called views under the spec folder. In our example, this is a view that corresponds to the index action of the review controller. We need to create anIndexSpec.php for our index.phtml view, containing the following code:
namespace Review;
 
// SpecHelper contains the usual ZF bootstrap
// copied from public/index.php
require_once __DIR__ . '/../../SpecHelper.php';
 
use \PHPSpec\Context\Zend\View as ViewContext;
 
class DescribeIndex extends ViewContext
{
    function itRendersTheSelectedVideo()
    {
        $video = \Mockery::mock(
            'Application_Model_Video', array('getName' => 'Revolution OS'));
        $this->assign('video', $video);
        $this->render();
        $this->rendered->should->contain('Revolution OS');
    }
}
Notice that we namespaced our class with the name of the controller. In a modular Zend Framework application you would namespace with the module name and then controller name, e.g. Module\Controller.
If we run the spec we should see an error because we haven’t met its description yet.
$ phpspec spec
E
 
Exceptions:
 
  1) Review\Index renders the selected video
     Failure\Exception: $this->runExamples($exampleGroup, $reporter);
     Zend_View_Exception: script 'review/index.phtml' not found in path (/private/var/www/renting/application/views/scripts/)
 
Finished in 0.004656 seconds
1 example, 1 exception
At this stage we need to create the view to deal with the error we saw. In this example, we will use Zend Tool, which creates the controller/action/view all in one go:
$ zf create controller Review
Creating a controller at /private/var/www/renting/application/controllers/ReviewController.php
Creating an index action method in controller Review
Creating a view script for the index action method at 
/private/var/www/renting/application/views/scripts/review/index.phtml
Updating project profile '/private/var/www/renting/.zfproject.xml'
Now instead of an error, our output shows that we have a failure:
$ phpspec spec
F
 
Failures:
 
  1) Review\Index renders the selected video
     Failure\Error: $this->rendered->should->contain('Revolution OS');
     expected to contain 'Revolution OS', found no match (using contain())
     # ./spec/views/review/IndexSpec.php:17
 
Finished in 0.005078 seconds
1 example, 1 failure
This output means that the view now exists, but it’s not showing the described text. That’s what we expect, since we haven’t coded the view yet. Let’s add some code to do so:
<?php echo $this->video->getName() ?>
It should now pass.
$ phpspec spec
.
 
Finished in 0.001043 seconds
1 example
That’s progress! However Behat is still not happy, because our controller is not setting the model for the view yet. Let us turn our focus to the controller, and start by creating a controllers directory and adding a ReviewControllerSpec.php.
DescribeReviewController must extend \PHPSpec\Context\Zend\Controller. We also need to specify that our controller will send be accessed by POST and will create the model in it. The controller/action will be routed from “/review” and we can add an example to make sure the route work as expected.
 
require_once __DIR__ . '/../SpecHelper.php';
 
class DescribeReviewController extends \PHPSpec\Context\Zend\Controller
{
    function itShouldRouteTheReviewsPageToTheIndexAction()
    {
        $this->routeFor(array('controller' => 'review','action' => 'index'))
             ->should->be('/review');
    }
 
    function itShouldDispatchToTheReviewController()
    {
        $container = new \Yadif_Container(
            new \Zend_Config_Xml(APPLICATION_PATH . "/configs/objects.xml")
        );
 
        $mapper = \Mockery::mock('Application_Model_VideoMapper');
        $mapper->shouldReceive('find')->andReturn($container->videoModel)->with('1')->once();
        $container->videoMapper = $mapper;
        $this->_getZendTest()->bootstrap->getBootstrap()->setContainer($container);
 
        $this->post('/review', array('id' => '1'));
    }
}
We’ll also want to add the code for the controller itself. Assuming you are using Yadif or another IoC (Inversion of Control) container, it would look like this:
 
class ReviewController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $this->view->video = $this->getMapper()->find($this->_request->id);
    }
 
    function getMapper()
    {
        $container = $this->getInvokeArg('bootstrap')->getContainer();
        return $container->getComponent('videoMapper');
    }
}
We’ll see that this now passes, when we run:
$ phpspec spec
...
 
Finished in 0.005271 seconds
3 examples
This looks much healthier, but if we run Behat, it will still be unhappy. We need to pass real data to the view, and then Behat will point out that the next step now is to describe the model. We need to have the data of our selected movies so we can fetch our view properly. Testing the models should be focused on behaviour, rather than the mechanics of database access. We know Zend_Db works, we need to know if we have made mistakes in our model. We will therefore test for the validation, filtering and business rules that we keep in the model.
class DescribeVideo extends \PHPSpec\Context
{
    function before()
    {
        $this->video = $this->spec(new Application_Model_Video);
    }
 
    function itIsNotValidWithoutTheName()
    {
        $this->video->setName('');
        $this->video->shouldNot->beValid();
    }
}
We need to implement a isValid() method in the Video model. PHPSpec does not have a beValid matcher, but it will use predicate matchers and find a method isValid() by magic. Once the isValid() is implemented properly than the spec passes. You can do something similar if the form validation is stored in your (zend) forms. Also note the before() method that will be called before any example is run.
You can describe your mapper’s behaviour by inspecting that it calls the Data Access Object (DAO) to fetch to or persist data from the models. At that point you can then add the DAO (e.g. DbTable, Rss, etc).

Should we Hit the Database?

In many cases you simply need to verify the business logic and not the database operation. Hitting the database consumes both time and resources, which will slow down the execution. There will be times where we need to expose some database behaviour or just feel more confident that our models work. In those cases we can use a Test Data Builder pattern (this is a topic on its own, and I will save it for another post).
If you run Behat again after the model specs pass, the view should be displaying the correct data, so your scenario should pass, and a new scenario will be failing. We use Behat to tell us what to do next, and so the outside-in cycle is begins again.

Final Thoughts

In BDD, developers are driven by the specification, rather than tests of things they haven’t written yet. That said, the goals of both TDD and BDD are basically the same: making sure the user gets what they want. BDD makes that focus more explicit, and uses a language that invites the user to write their tests as a specification.
Behat and PHPSpec sit at different levels in the Outside-in cycle. Behat provides the outermost layer, allowing the stakeholders and developers to collaborate. PHPSpec provides the inner layer, allowing the specification of how the classes will collaborate with each other.


Nenhum comentário :

Postar um comentário

Total de visualizações de página