In the second part of this series called Laravel, BDD and You, we will start describing and building our first feature using Behat and PhpSpec. In the last article we got everything set up and saw how easily we can interact with Laravel in our Behat scenarios.
Recently the creator of Behat, Konstantin Kudryashov (a.k.a. everzet), wrote a really great article called Introducing Modelling by Example. The workflow we are going to use, when we build our feature, is highly inspired by the one presented by everzet.
In short, we are going to use the same .feature
to design both our core domain and our user interface. I have often felt that I had a lot of duplication in my features in my acceptance/functional and integration suites. When I read everzet's suggestion about using the same feature for multiple contexts, it all clicked for me and I believe it is the way to go.
In our case we will have our functional context, which will, for now, also serve as our acceptance layer, and our integration context, which will cover our domain. We will start by building the domain and then add the UI and framework-specific things afterwards.
Small Refactorings
In order to use the "shared feature, multiple contexts" approach, we have to do a few refactorings of our existing setup.
First, we are going to delete the welcome feature we did in the first part, since we do not really need it and it does not really follow the generic style we need in order to use multiple contexts.
$ git rm features/functional/welcome.feature
Second, we are going to have our features in the root of the features
folder, so we can go ahead and remove the path
attribute from our behat.yml
file. We are also going to rename the LaravelFeatureContext
to FunctionalFeatureContext
(remember to change the class name as well):
default: suites: functional: contexts: [ FunctionalFeatureContext ]
Finally, just to clean things up a bit, I think we should move all Laravel-related stuff into its own trait:
# features/bootstrap/LaravelTrait.php <?php use Illuminate\Foundation\Testing\ApplicationTrait; trait LaravelTrait { /** * Responsible for providing a Laravel app instance. */ use ApplicationTrait; /** * @BeforeScenario */ public function setUp() { if ( ! $this->app) { $this->refreshApplication(); } } /** * Creates the application. * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../bootstrap/start.php'; } }
In the FunctionalFeatureContext
we can then use the trait and delete the things we just moved:
/** * Behat context class. */ class FunctionalFeatureContext implements SnippetAcceptingContext { use LaravelTrait; /** * Initializes context. * * Every scenario gets its own context object. * You can also pass arbitrary arguments to the context constructor through behat.yml. */ public function __construct() { }
Traits are a great way to clean up your contexts.
Sharing a Feature
As presented in part one, we are going to build a small application for time tracking. The first feature is going to be about tracking time and generating a time sheet from the tracked entries. Here is the feature:
Feature: Tracking time In order to track time spent on tasks As an employee I need to manage a time sheet with time entries Scenario: Generating time sheet Given I have the following time entries | task | duration | | coding | 90 | | coding | 30 | | documenting | 150 | When I generate the time sheet Then my total time spent on coding should be 120 minutes And my total time spent on documenting should be 150 minutes And my total time spent on meetings should be 0 minutes And my total time spent should be 270 minutes
Remember that this is only an example. I find it easier to define features in real life, since you have an actual problem you need to solve and often get the chance to discuss the feature with colleagues, clients, or other stakeholders.
Okay, let us have Behat generate the scenario steps for us:
$ vendor/bin/behat --dry-run --append-snippets
We need to tweak the generated steps just a tiny bit. We only need four steps to cover the scenario. The end result should look something like this:
/** * @Given I have the following time entries */ public function iHaveTheFollowingTimeEntries(TableNode $table) { throw new PendingException(); } /** * @When I generate the time sheet */ public function iGenerateTheTimeSheet() { throw new PendingException(); } /** * @Then my total time spent on :task should be :expectedDuration minutes */ public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) { throw new PendingException(); } /** * @Then my total time spent should be :expectedDuration minutes */ public function myTotalTimeSpentShouldBeMinutes($expectedDuration) { throw new PendingException(); }
Our functional context is all ready to go now, but we also need a context for our integration suite. First, we will add the suite to the behat.yml
file:
default: suites: functional: contexts: [ FunctionalFeatureContext ] integration: contexts: [ IntegrationFeatureContext ]
Next, we can just copy the default FeatureContext
:
$ cp features/bootstrap/FeatureContext.php features/bootstrap/IntegrationFeatureContext.php
Remember to change the class name to IntegrationFeatureContext
and also to copy the use statement for the PendingException
.
Finally, since we are sharing the feature, we can just copy the four step definitions from the functional context. If you run Behat, you will see that the feature is run twice: once for each context.
Designing the Domain
At this point, we are ready to start filling out the pending steps in our integration context in order to design the core domain of our application. The first step is Given I have the following time entries
, followed by a table with time entry records. Keeping it simple, let us just loop over the rows of the table, try to instantiate a time entry for each of them, and add them to an entries array on the context:
use TimeTracker\TimeEntry; ... /** * @Given I have the following time entries */ public function iHaveTheFollowingTimeEntries(TableNode $table) { $this->entries = []; $rows = $table->getHash(); foreach ($rows as $row) { $entry = new TimeEntry; $entry->task = $row['task']; $entry->duration = $row['duration']; $this->entries[] = $entry; } }
Running Behat will cause a fatal error, since the TimeTracker\TimeEntry
class does not yet exist. This is where PhpSpec enters the stage. In the end, TimeEntry
is going to be an Eloquent class, even though we do not worry about it yet. PhpSpec and ORMs like Eloquent do not play together that well, but we can still use PhpSpec to generate the class and even spec out some basic behavior. Let us use the PhpSpec generators to generate the TimeEntry
class:
$ vendor/bin/phpspec desc "TimeTracker\TimeEntry" $ vendor/bin/phpspec run Do you want me to create `TimeTracker\TimeEntry` for you? y
After the class is generated, we need to update the autoload section of our composer.json
file:
"autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds" ], "psr-4": { "TimeTracker\\": "src/TimeTracker" } },
And of course run composer dump-autoload
.
Running PhpSpec gives us green. Running Behat gives us green as well. What a great start!
Letting Behat guide our way, how about we just move along to the next step, When I generate the time sheet
, right away?
The keyword here is "generate", which looks like a term from our domain. In a programmer's world, translating "generate the timesheet" to code could just mean instantiating a TimeSheet
class with a bunch of time entries. It is important to try and stick to the language from the domain when we design our code. That way, our code will help describe the intended behavior of our application.
I identify the term generate
as important for the domain, which is why I think we should have a static generate method on a TimeSheet
class that serves as an alias for the constructor. This method should take a collection of time entries and store them on the time sheet.
Instead of just using an array, I think it will make sense to use the Illuminate\Support\Collection
class that comes with Laravel. Since TimeEntry
will be an Eloquent model, when we query the database for time entries, we will get one of these Laravel collections anyway. How about something like this:
use Illuminate\Support\Collection; use TimeTracker\TimeSheet; use TimeTracker\TimeEntry; ... /** * @When I generate the time sheet */ public function iGenerateTheTimeSheet() { $this->sheet = TimeSheet::generate(Collection::make($this->entries)); }
By the way, TimeSheet is not going to be an Eloquent class. At least for now, we only need to make the time entries persist, and then the time sheets will just be generated from the entries.
Running Behat will, once again, cause a fatal error, because TimeSheet
does not exist. PhpSpec can help us solve that:
$ vendor/bin/phpspec desc "TimeTracker\TimeSheet" $ vendor/bin/phpspec run Do you want me to create `TimeTracker\TimeSheet` for you? y $ vendor/bin/phpspec run $ vendor/bin/behat PHP Fatal error: Call to undefined method TimeTracker\TimeSheet::generate()
We still get a fatal error after creating the class, because the static generate()
method still does not exist. Since this is a really simple static method, I do not think there is a need for a spec. It is nothing more than a wrapper for the constructor:
<?php namespace TimeTracker; use Illuminate\Support\Collection; class TimeSheet { protected $entries; public function __construct(Collection $entries) { $this->entries = $entries; } public static function generate(Collection $entries) { return new static($entries); } }
This will get Behat back to green, but PhpSpec is now squeaking at us, saying: Argument 1 passed to TimeTracker\TimeSheet::__construct() must be an instance of Illuminate\Support\Collection, none given
. We can solve this by writing a simple let()
function that will be called before each spec:
<?php namespace spec\TimeTracker; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Illuminate\Support\Collection; use TimeTracker\TimeEntry; class TimeSheetSpec extends ObjectBehavior { function let(Collection $entries) { $entries->put(new TimeEntry); $this->beConstructedWith($entries); } function it_is_initializable() { $this->shouldHaveType('TimeTracker\TimeSheet'); } }
This will get us back to green all over the line. The function makes sure that the time sheet is always constructed with a mock of the Collection class.
We can now safely move on to the Then my total time spent on...
step. We need a method that takes a task name and return the accumulated duration of all entries with this task name. Directly translated from gherkin to code, this could be something like totalTimeSpentOn($task)
:
/** * @Then my total time spent on :task should be :expectedDuration minutes */ public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) { $actualDuration = $this->sheet->totalTimeSpentOn($task); PHPUnit::assertEquals($expectedDuration, $actualDuration); }
The method does not exist, so running Behat will give us Call to undefined method TimeTracker\TimeSheet::totalTimeSpentOn()
.
In order to spec out the method, we will write a spec that looks somehow similar to what we already have in our scenario:
function it_should_calculate_total_time_spent_on_task() { $entry1 = new TimeEntry; $entry1->task = 'sleeping'; $entry1->duration = 120; $entry2 = new TimeEntry; $entry2->task = 'eating'; $entry2->duration = 60; $entry3 = new TimeEntry; $entry3->task = 'sleeping'; $entry3->duration = 120; $collection = Collection::make([$entry1, $entry2, $entry3]); $this->beConstructedWith($collection); $this->totalTimeSpentOn('sleeping')->shouldBe(240); $this->totalTimeSpentOn('eating')->shouldBe(60); }
Note that we do not use mocks for the TimeEntry
and Collection
instances. This is our integration suite and I do not think there is a need to mock this out. The objects are quite simple and we want to make sure that the objects in our domain interact as we expect them to. There are probably many opinions about this, but this makes sense to me.
Moving along:
$ vendor/bin/phpspec run Do you want me to create `TimeTracker\TimeSheet::totalTimeSpentOn()` for you? y $ vendor/bin/phpspec run 25 ✘ it should calculate total time spent on task expected [integer:240], but got null.
In order to filter the entries, we can use the filter()
method on the Collection
class. A simple solution that gets us to green:
public function totalTimeSpentOn($task) { $entries = $this->entries->filter(function($entry) use ($task) { return $entry->task === $task; }); $duration = 0; foreach ($entries as $entry) { $duration += $entry->duration; } return $duration; }
Our spec is green, but I feel that we could benefit from some refactoring here. The method seems to do two different things: filter entries and accumulate the duration. Let us extract the latter to its own method:
public function totalTimeSpentOn($task) { $entries = $this->entries->filter(function($entry) use ($task) { return $entry->task === $task; }); return $this->sumDuration($entries); } protected function sumDuration($entries) { $duration = 0; foreach ($entries as $entry) { $duration += $entry->duration; } return $duration; }
PhpSpec is still green and we now have three green steps in Behat. The last step should be easy to implement, since it is somewhat similar to the one we just did.
/** * @Then my total time spent should be :expectedDuration minutes */ public function myTotalTimeSpentShouldBeMinutes($expectedDuration) { $actualDuration = $this->sheet->totalTimeSpent(); PHPUnit::assertEquals($expectedDuration, $actualDuration); }
Running Behat will give us Call to undefined method TimeTracker\TimeSheet::totalTimeSpent()
. Instead of doing a separate example in our spec for this method, how about we just add it to the one we already have? It might not be completely in line with what is "right" to do, but let us be a little pragmatic:
... $this->beConstructedWith($collection); $this->totalTimeSpentOn('sleeping')->shouldBe(240); $this->totalTimeSpentOn('eating')->shouldBe(60); $this->totalTimeSpent()->shouldBe(300);
Let PhpSpec generate the method:
$ vendor/bin/phpspec run Do you want me to create `TimeTracker\TimeSheet::totalTimeSpent()` for you? y $ vendor/bin/phpspec run 25 ✘ it should calculate total time spent on task expected [integer:300], but got null.
Getting to green is easy now that we have the sumDuration()
method:
public function totalTimeSpent() { return $this->sumDuration($this->entries); }
And now we have a green feature. Our domain is slowly evolving!
Designing the User Interface
Now, we are moving to our functional suite. We are going to design the user interface and deal with all the Laravel-specific stuff that is not the concern of our domain.
While working in the functional suite, we can add the -s
flag to instruct Behat to only run our features through the FunctionalFeatureContext
:
$ vendor/bin/behat -s functional
The first step is going to look similar to the first one of the integration context. Instead of just making the entries persist on the context in an array, we need to actually make them persist in a database so that they can be retrieved later:
use TimeTracker\TimeEntry; ... /** * @Given I have the following time entries */ public function iHaveTheFollowingTimeEntries(TableNode $table) { $rows = $table->getHash(); foreach ($rows as $row) { $entry = new TimeEntry; $entry->task = $row['task']; $entry->duration = $row['duration']; $entry->save(); } }
Running Behat will give us fatal error Call to undefined method TimeTracker\TimeEntry::save()
, since TimeEntry
still is not an Eloquent model. That is easy to fix:
namespace TimeTracker; class TimeEntry extends \Eloquent { }
If we run Behat again, Laravel will complain that it cannot connect to the database. We can fix this by adding a database.php
file to the app/config/testing
directory, in order to add the connection details for our database. For larger projects, you probably want to use the same database server for your tests and your production code base, but in our case, we will just use an in memory SQLite database. This is super simple to set up with Laravel:
<?php return array( 'default' => 'sqlite', 'connections' => array( 'sqlite' => array( 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ), ), );
Now if we run Behat, it will tell us that there is no time_entries
table. In order to fix this, we need to make a migration:
$ php artisan migrate:make createTimeEntriesTable --create="time_entries"
Schema::create('time_entries', function(Blueprint $table) { $table->increments('id'); $table->string('task'); $table->integer('duration'); $table->timestamps(); });
We are still not green, since we need a way to instruct Behat to run our migrations before every scenario, so we have a clean slate every time. By using Behat's annotations, we can add these two methods to the LaravelTrait
trait:
/** * @BeforeScenario */ public function setupDatabase() { $this->app['artisan']->call('migrate'); } /** * @AfterScenario */ public function cleanDatabase() { $this->app['artisan']->call('migrate:reset'); }
This is pretty neat and gets our first step to green.
Next up is the When I generate the time sheet
step. The way I see it, generating the time sheet is the equivalent of visiting the index
action of the time entry resource, since the time sheet is the collection of all the time entries. So the time sheet object is like a container for all the time entries and gives us a nice way to handle entries. Instead of going to /time-entries
, in order to see the time sheet, I think the employee should go to /time-sheet
. We should put that in our step definition:
/** * @When I generate the time sheet */ public function iGenerateTheTimeSheet() { $this->call('GET', '/time-sheet'); $this->crawler = new Crawler($this->client->getResponse()->getContent(), url('/')); }
This will cause a NotFoundHttpException
, since the route is not defined yet. As I just explained, I think this URL should map to the index
action on the time entry resource:
Route::get('time-sheet', ['as' => 'time_sheet', 'uses' => 'TimeEntriesController@index']);
In order to get to green, we need to generate the controller:
$ php artisan controller:make TimeEntriesController $ composer dump-autoload
And there we go.
Finally, we need to crawl the page to find the total duration of the time entries. I reckon we will have some sort of table that summarises the durations. The last two steps are so similar that we are just going to implement them at the same time:
/** * @Then my total time spent on :task should be :expectedDuration minutes */ public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) { $actualDuration = $this->crawler->filter('td#' . $task . 'TotalDuration')->text(); PHPUnit::assertEquals($expectedDuration, $actualDuration); } /** * @Then my total time spent should be :expectedDuration minutes */ public function myTotalTimeSpentShouldBeMinutes($expectedDuration) { $actualDuration = $this->crawler->filter('td#totalDuration')->text(); PHPUnit::assertEquals($expectedDuration, $actualDuration); }
The crawler is looking for a <td>
node with an id of [task_name]TotalDuration
or totalDuration
in the last example.
Since we still do not have a view, the crawler will tell us that The current node list is empty.
In order to fix this, let us build the index
action. First, we fetch the collection of time entries. Second, we generate a time sheet from the entries and send it along to the (still non-existing) view.
use TimeTracker\TimeSheet; use TimeTracker\TimeEntry; class TimeEntriesController extends \BaseController { /** * Display a listing of the resource. * * @return Response */ public function index() { $entries = TimeEntry::all(); $sheet = TimeSheet::generate($entries); return View::make('time_entries.index', compact('sheet')); } ...
The view, for now, is just going to consist of a simple table with the summarised duration values:
<h2>Time Sheet</h2> <table> <thead> <th>Task</th> <th>Total duration</th> </thead> <tbody> <tr> <td>coding</td> <td id="codingTotalDuration">{{ $sheet->totalTimeSpentOn('coding') }}</td> </tr> <tr> <td>documenting</td> <td id="documentingTotalDuration">{{ $sheet->totalTimeSpentOn('documenting') }}</td> </tr> <tr> <td>meetings</td> <td id="meetingsTotalDuration">{{ $sheet->totalTimeSpentOn('meetings') }}</td> </tr> <tr> <td><strong>Total</strong></td> <td id="totalDuration">{{ $sheet->totalTimeSpent() }}</td> </tr> </tbody> </table>
If you run Behat again, you will see that we successfully implemented the feature. Maybe we should take a moment to realise that not even once did we open up a browser! This is a massive improvement to our workflow, and as a nice bonus, we now have automated tests for our application. Yay!
Conclusion
If you run vendor/bin/behat
in order to run both Behat suites, you will see that both of them are green now. If you run PhpSpec though, unfortunately, you will see that our specs are broken. We get a fatal error Class 'Eloquent' not found in ...
. This is because Eloquent is an alias. If you take a look in app/config/app.php
under aliases, you will see that Eloquent
is actually an alias for Illuminate\Database\Eloquent\Model
. In order to get PhpSpec back to green, we need to import this class:
namespace TimeTracker; use Illuminate\Database\Eloquent\Model as Eloquent; class TimeEntry extends Eloquent { }
If you run these two commands:
$ vendor/bin/phpspec run; vendor/bin/behat
You will see that we are back to green, both with Behat and PhpSpec. Yay!
We have now described and designed our first feature, completely using a BDD approach. We have seen how we can benefit from designing the core domain of our application, before we worry about the UI and the framework specific stuff. We have also seen how easy it is to interact with Laravel, and especially the database, in our Behat contexts.
In the next article, we are going to do a lot of refactoring in order to avoid too much logic on our Eloquent models, since these are more difficult to test in isolation and are tightly coupled to Laravel. Stay tuned!
Comments