Laravel, BDD and You: The First Feature

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.

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):

Finally, just to clean things up a bit, I think we should move all Laravel-related stuff into its own trait:

In the FunctionalFeatureContext we can then use the trait and delete the things we just moved:

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:

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:

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:

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:

Next, we can just copy the default FeatureContext:

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:

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:

After the class is generated, we need to update the autoload section of our composer.json file:

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:

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:

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:

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:

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):

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:

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:

In order to filter the entries, we can use the filter() method on the Collection class. A simple solution that gets us to green:

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:

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.

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:

Let PhpSpec generate the method:

Getting to green is easy now that we have the sumDuration() method:

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:

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:

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:

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:

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:

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:

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:

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:

In order to get to green, we need to generate the controller:

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:

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.

The view, for now, is just going to consist of a simple table with the summarised duration values:

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:

If you run these two commands:

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!

Tags:

Comments

Related Articles