Aug 27 2014
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:
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 photos When a client views a completed photo shoot Then 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.client visit new_session_path fill_in "email", :with => client.email fill_in "password", :with => client.password click_button "Submit" visit photo_shoot_path(@photo_shoot) end Then(/^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) end end
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.
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_shoot sign_in @photo_shoot.client visit photo_shoot_path(@photo_shoot) end Then(/^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) end end
module SessionActions def sign_in(client) visit new_session_path fill_in "email", :with => @client.email fill_in "password", :with => @client.password click_button "Submit" end end
This is better. We abstracted what it means to have a completed photo shoot, by introducing a
create_completed_photo_shoot factory method for the photo shoot, and we also abstracted what it means to sign in. If you have a simple app, this may well be enough.
Unfortunately, most apps aren’t simple.
As an application’s complexity increases, test implementation becomes trickier. Consider:
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_shoot roles.client. sign_in(@photo_shoot.client). view_photo_shoot @photo_shoot end Then(/^they should see the list of all its photos$/) do expect(roles.client).to see :photos, @photo_shoot.photos end
module Roles def roles @roles ||= OpenStruct.new(:client => Client.new) end end
module Roles class Client < Dill::Role # This is a quick way to define a form widget. form :sign_in, "#new_session" do text_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_path submit :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. self end # Here, we declare a widget of type List. There is no #list # helper yet, but it’s in the plans widget :photos, '#photos', Dill::List do item '.photo .name' end def view_photo_shoot(photo_shoot) visit photo_shoot_path photo_shoot self end # 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).sort end end end
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
visit <location>, or
submit <form>, let you write the code as you would talk about it, making it slightly more readable (but don’t worry, you’re free to use the standard OO form as well).
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.
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.