In my previous article, we discussed how we should write living documentation. In this article, we’ll explore how we can write the code underlying the documentation. As always, I’ll use Cucumber for the examples, but I believe these principles apply independent of the tool you use.
We previously saw that living documentation mainly describes two different areas of the system:
- The system behavior--how the system interacts (changes or uses information) with the business domain.
- The system domain--the system’s entities, and the properties that make up their essence.
In this article, we’ll see how we can write tests for system behavior. We’ll explore domain entity tests in a future article.
Consider the following scenario from my previous article:
Scenario: letting clients view photosWhen a client views a completed photo shootThen they should see the list of all its photos<Paste>
And here’s a possible implementation, written as an end-to-end test:
When(/^a client views a completed photo shoot$/) do@photo_shoot = db.create_photo_shoot(:shot_at => Time.now,:photos => [db.build_photo])client = @photo_shoot.clientvisit new_session_pathfill_in "email", :with => client.emailfill_in "password", :with => client.passwordclick_button "Submit"visit photo_shoot_path(@photo_shoot)endThen(/^they should see the list of all its photos$/) do@photo_shoot.photos.each do |e|expect(page).to have_selector(".photo .name", text: e.name)endend
We have a feature file, and a corresponding step definition file. I seldom write reusable steps, because step definitions make for lousy reusable units (Ruby already provides us all the reuse facilities we need, thankyouverymuch). So, we don’t have to worry about putting the steps in a file with a name that’s general enough (often several different file names would make sense, making the steps harder to find).
But there are a few problems with the way these steps have been implemented:
First, we are making some assumptions about what a completed photo shoot means. In my previous article, I explained that a photo shoot is considered “completed” when it has a date of shooting and contains at least one photo. Here, we’re not making use of the “completed” abstraction--we’re hard-coding its meaning.
Second, almost nothing is being reused. For example, there’s no way to use the sign in code in other features, if we need to.
Third, the first step definition is big! Steps definitions are somewhat awkward to work with, so they should be kept small.
If at first you don’t succeed...
It’s easy to fix most of the problems discussed above. A second iteration could look like this:
When(/^a client views a completed photo shoot$/) do@photo_shoot = db.create_completed_photo_shootsign_in @photo_shoot.clientvisit photo_shoot_path(@photo_shoot)endThen(/^they should see the list of all its photos$/) do@photo_shoot.photos.each do |e|expect(page).to have_selector(".photo .name", text: e.name)endend
module SessionActionsdef sign_in(client)visit new_session_pathfill_in "email", :with => @client.emailfill_in "password", :with => @client.passwordclick_button "Submit"endend
This is better. We abstracted what it means to have a completed photo shoot, by introducing a
Unfortunately, most apps aren’t simple.
When the going gets tough...
As an application’s complexity increases, test implementation becomes trickier. Consider:
- Different kinds of users use different parts of the app.
- The same user uses different parts of the app, depending on their goals.
- Different kinds of users may need to use the same part of the app in slightly different ways.
To deal with these complexities, I created the Dill library. It’s very much a work in progress, so it has a few rough parts, but it’s been helping me deal with the complexities of UI testing (using Cucumber and Rails) in a fairly structured way.
We usually think of people using our app, whatever they do, as simple users, but users put on different roles depending on the goals they have at the moment. For example, a user trying to change their email or password is currently putting on the role of an account manager. A user browsing your store is acting as a prospect, and once they checkout, they become a client.
To achieve their goals, roles need to perform certain meaningful business actions. For example, change password, checkout, or browse product list are all actions a user may perform while putting on different roles. To accomplish an action, a role may start by visiting a certain page, then clicking a link, or submitting a form with some information.
To effectively accomplish anything, actions manipulate widgets. Widgets are declarative wrappers around HTML elements. Dill gives you a DSL that lets you declare and interact with widgets in a fluent manner.
The following code is an annotated example of how we’d implement the step definitions using Dill:
When(/^a client views a completed photo shoot$/) do@photo_shoot = db.create_completed_photo_shootroles.client.sign_in(@photo_shoot.client).view_photo_shoot @photo_shootendThen(/^they should see the list of all its photos$/) doexpect(roles.client).to see :photos, @photo_shoot.photosend
module Rolesdef roles@roles ||= OpenStruct.new(:client => Client.new)endend
module Rolesclass Client < Dill::Role# This is a quick way to define a form widget.form :sign_in, "#new_session" dotext_field :email, "email"text_field :password, "password"end# The #sign_in action, as its name indicates, signs a client in.# Note how the DSL allows you to express yourself in terms of the# (UI) domain.def sign_in(user)visit new_session_pathsubmit :sign_in, :email => user.email,:password => user.password# we return the role so we can chain method calls. Sometimes,# you may want to return a *different* role instead.selfend# Here, we declare a widget of type List. There is no #list# helper yet, but it’s in the planswidget :photos, '#photos', Dill::List doitem '.photo .name'enddef view_photo_shoot(photo_shoot)visit photo_shoot_path photo_shootselfend# Used by the #see matcher (see the step definition above).def see_photos?(photos)# #value converts the widget into a ruby data structure. By# default, a List return an array of strings containing the# text of each of its items. This is usually what you want, but# you can override a widget’s value by defining your own #value# method inside the widget.value(:photos).sort == photos.map(&:name).sortendendend
As you can see, Dill tries to help you write code that is as declarative as possible. We visit a location, submit a form, check the value of a widget, without worrying much about implementation details. UI interactions provided by Dill, like
Additionally, Dill helps you structure your test code. You just need to find the appropriate role, or maybe create one. If you want roles to share some actions, just create a module and include it in the relevant roles.
Finally, step definitions are still small and very readable. The first layer describes how the domain is setup, what each role does and observes (some scenarios will also check the state of the domain, although that’s not shown in this case). The second layer describes the UI steps taken, while still keeping it abstracted.
End-to-end is not the end
You should now have the tools to write better structured, more manageable, end-to-end tests in Ruby, using Cucumber. If you want to find out more about Dill, you can head on to the GitHub page, or take a look at the living documentation on Relish.
But now, I’d like to close with an alert.
Developers widely believe that Cucumber is just an acceptance testing tool. I disagree. I treat Cucumber as a living documentation tool, and it doesn’t really matter what kind of test underlies the documentation, as long as it allows you to be reasonably confident that the feature you implemented works according to the specification.
An implication of this is that you won’t always need to write an end-to-end test when you write a Cucumber feature. End-to-end tests are slow (especially if running in a real browser) and they exercise a lot of code, sometimes making it hard to find where a problem lies. It may be enough to deal directly with the API endpoint. Entity tests don’t even touch controllers.
No test implementation is final. Properly written scenarios will help you move seamlessly from one test implementation to another. As long as the business rule still applies, the documentation doesn’t change. Only the code does.
And we’re done. In the next article I plan to go into how to use Cucumber and Dill to bring the benefits of living documentation to areas traditionally covered by lower level tests that preserve the fluency of Gherkin but do not require the overhead of browser based testing.