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)