Functional testing with Behat

While phpspec follows the BDD by specification and is useful for specification and design in isolation, its complimentary tool Behat is used for integration and functional tests. Since phpspec suggests to mock everything, database queries wouldn't actually be executed, as the database is outside the context of that method. Behat is a great tool to perform behavioral testing on a certain feature. While phpspec is already included among Laravel 5's dependencies, Behat will be installed as an external module.

The following command should be run to install and make Behat work with Laravel 5:

$ composer require behat/behat behat/mink behat/mink-extension laracasts/behat-laravel-extension --dev

After running the composer update, Behat's functionality is added to Laravel. Next, a behat.yaml file should be added to the root of the Laravel project to specify which extensions are to be used.

Next, run the following command:

$ behat --init

This will create a features directory with a bootstrap directory inside it. A FeaturesContext class will also be created. Everything inside bootstrap will be run every time behat is run. This is useful to automatically run migrations and seeding.

The features/bootstrap/FeaturesContext.php file looks like this:

<?php

use BehatBehatContextContext;
use BehatBehatContextSnippetAcceptingContext;
use BehatGherkinNodePyStringNode;
use BehatGherkinNodeTableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
    }
}

Next, the FeatureContext class needs to extend the MinkContext class, so the class definition line will need to be modified as follows:

class FeatureContext implements Context, SnippetAcceptingContext

Next, the prepare and cleanup methods will be added to the class in order to perform the migrations. We will add the @BeforeSuite and @AfterSuite annotations to tell Behat to perform the migration and seeding before each suite and migrate to rollback in order to restore the database to its original state after each suite. Using annotations in the doc-block will be discussed in Chapter 6, Taming Complexity with Annotations. Our class now is structured as follows:

<?php

use BehatBehatContextContext;
use BehatBehatContextSnippetAcceptingContext;
use BehatGherkinNodePyStringNode;
use BehatGherkinNodeTableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
    }
     /**
     * @BeforeSuite
     */
     public static function prepare(SuiteEvent $event)
     {
        Artisan::call('migrate'),
        Artisan::call('db:seed'),

     }

     /**
     * @AfterSuite 
     */
     public function cleanup(ScenarioEvent $event)
     {
        Artisan::call('migrate:rollback'),
     }
}

Now, a feature file needs to be created. Create reservation.feature in the room directory:

Feature: Reserve Room
  In order to verify the reservation system
  As an accommodation reservation user
  I need to be able to create a reservation in the system
  Scenario: Reserve a Room
   When I create a reservation
         Then I should have one reservation

When behat is run as follows:

$ behat

The following output is produced:

Feature: Reserve Room
  In order to verify the reservation system
  As an accommodation reservation user
  I need to be able to create a reservation in the system

  Scenario: List 2 files in a directory # features/reservation.feature:5
    When I create a reservation
    Then I should have one reservation

1 scenario (1 undefined)
2 steps (2 undefined)
0m0.10s (7.48Mb)

--- FeatureContext has missing steps. Define them with these snippets:

    /**
     * @When I create a reservation
     */
    public function iCreateAReservation()
    {
        throw new PendingException();
    }

    /**
     * @Then I should have one reservation
     */
    public function iShouldHaveOneReservation()
    {
        throw new PendingException();
    }

Behat, as did phpspec, skillfully produces the output, showing you the methods that need to be created. Notice that camel case is used instead of snake case. This code should be copied in to the FeatureContext class. Notice that, by default, an exception is thrown.

Here, the RESTful API will be called, so the guzzle HTTP package will need to be added to the project:

$ composer require guzzlehttp/guzzle

Next, add an attribute to the class to hold the guzzle object. We will add a POST request to a RESTful resource controller to create a reservation and expect a 201 code. Notice that the return code is a string and needs to be casted to an integer. Next, a get is performed to return all of the reservations.

There should only be one reservation created, since the migration and seeding run every time:

<?php

use BehatBehatContextContext;
use BehatBehatContextSnippetAcceptingContext;
use BehatGherkinNodePyStringNode;
use BehatGherkinNodeTableNode;
use BehatMinkExtensionContextMinkContext;
use BehatTestworkHookScopeBeforeSuiteScope;
use BehatTestworkHookScopeAfterSuiteScope;
use GuzzleHttpClient;

/**
 * Defines application features from the specific context.
 */
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    protected $httpClient;

    public function __construct()
    {
        $this->httpClient = new Client();
    }
    /**
     * @BeforeSuite
     */
    public static function prepare(BeforeSuiteScope $scope)
    {
        Artisan::call('migrate'),
        Artisan::call('db:seed'),

    }

    /**
     * @When I create a reservation
     */
    public function iCreateAReservation()
    {
        $request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
        if ((int)$request->getStatusCode()!==201)
        {
            throw new Exception('A successfully created status code must be returned'),
        }
    }

    /**
     * @Then I should have one reservation
     */
    public function iShouldHaveOneReservation()
    {
        $request = $this->httpClient->get('http://laravel.example/reservations'),
        $arr = json_decode($request->getBody());
        if (count($arr)!==1)
        {
            throw new Exception('there must be exactly one reservation'),
        }
    }

    /**
     * @AfterSuite
     */
    public static function cleanup(AfterSuiteScope $scope)
    {
        Artisan::call('migrate:rollback'),
    }
}

    /**
     * @When I create a reservation
     */
    public function iCreateAReservation()
    {
        $request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
        if ((int)$request->getStatusCode()!==201)
        {
            throw new Exception('A successfully created status code must be returned'),
        }
    }

Now, to create ReservationController, use artisan from the command line:

$ php artisan make:controller ReservationsController

Here are the contents of the reservation controller:

<?php namespace MyCompanyHttpControllers;

use MyCompanyHttpRequests;
use MyCompanyHttpControllersController;
use IlluminateHttpRequest;
use SymfonyComponentHttpFoundationResponse;
use MyCompanyAccommodationReservationRepository;
use MyCompanyAccommodationReservationValidator;
use MyCompanyAccommodationReservation;

class ReservationsController extends Controller {

    /**
    * Display a listing of the resource.
    *
    * @return Response
    */
    public function index()
    {
        return Reservation::all();
    }

    /**
    * Store a newly created resource in storage.
    *
    * @return Response
    */
    public function store()
    {
        $reservationRepository = new ReservationRepository(new Reservation());
        $reservationValidator = new ReservationValidator();
        if ($reservationValidator->validate(Input::get('start_date'),
        Input::get('end_date'),Input::get('rooms')))
        {
        $reservationRepository->create(['date_start'=>Input::get('start_date'),'date_end'=>Input::get('end_date'),'rooms'=>Input::get('rooms')]);
        return response('', '201'),
        }
    }
}

Lastly, add ReservationController to the routes.php file, which is located in app/Http/routes.php:

Route::resource('reservations','ReservationController'),

Now, when behat is run, the result is as follows:

Feature: Reserve Room
  In order to verify the reservation system
  As an accommodation reservation user
  I need to be able to create a reservation in the system

  Scenario: Reserve a Room
    When I create a reservation         # FeatureContext::iCreateAReservation()
    Then I should have one reservation  # FeatureContext::iShouldHaveOneReservation()

1 scenario (1 passed)
2 steps (2 passed)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset