Email Spec
A collection of RSpec matchers and Cucumber steps to make testing emails go smoothly.
This library works with ActionMailer and Pony. When using it with ActionMailer it works with DelayedJob, ActiveRecord Mailer, and action_mailer_cache_delivery.
If you are testing emails in conjunction with an automated browser solution, like Selenium, you will want to use action_mailer_cache_delivery in your test environment. (This is because your test process and server processes are distinct and therefore need an intermediate store for the emails.) DelayedJob and ActiveRecord Mailer will also work but you generally don't want to include those projects unless you need them in production.
Setup
script/plugin install git://github.com/bmabey/email-spec.gitGem Setup
gem install email_spec # Gemfile group :test do gem 'email_spec' endCucumber
To use the steps in features put the following in your env.rb:
# Make sure this require is after you require cucumber/rails/world. require 'email_spec' # add this line if you use spork require 'email_spec/cucumber'This will load all the helpers that the steps rely on. It will also add a Before hook for Cucumber so that emails are cleared at the start of each scenario.
Then:
rails generate email_spec:stepsThis will give you a bunch of steps to get started with in step_definitions/email_steps.rb
By default, the generated file will look for email to example@example.com. You can either change this by editing the current_email_address method in email_steps.rb, or by simply specifying the target email in your features:
Scenario: A new person signs up Given I am at "/" When I fill in "Email" with "quentin@example.com" And I press "Sign up" And "quentin@example.com" should receive an email # Specify who should receive the emailRSpec
First you need to require email_spec in your spec_helper.rb:
You will then need to include EmailSpec::Helpers and EmailSpec::Matchers in your example groups. If you want to have access to the helpers and matchers in all of your examples you can do the following in your spec_helper.rb:
RSpec.configure do |config| config.include(EmailSpec::Helpers) config.include(EmailSpec::Matchers) endOtherwise, you will need to include them in the example groups you wish to use them:
describe "Signup Email" do include EmailSpec::Helpers include EmailSpec::Matchers ... endUsage
Cucumber
Scenario: A new person signs up Given I am at "/" When I fill in "Email" with "quentin@example.com" And I press "Sign up" And I should receive an email When I open the email Then I should see "confirm" in the email body When I follow "confirm" in the email Then I should see "Confirm your new account"For more examples, check out examples/rails_root in the source for a small example app that implements these steps.
Cucumber Matchers (Ruby)
See RSpec Matchers (they are the same)
RSpec
Testing In Isolation
It is often useful to test your mailers in isolation. You can accomplish this by using mocks to verify that the mailer is being called in the correct place and then write focused examples for the actual mailer. This is a simple example from the sample app found in the gem:
Verify that the mailer is used correctly in the controller (this would apply to a model as well):
describe "POST /signup (#signup)" do it "should deliver the signup email" do # expect UserMailer.should_receive(:deliver_signup).with("email@example.com", "Jimmy Bean") # when post :signup, "Email" => "email@example.com", "Name" => "Jimmy Bean" end endExamples for the #signup method in UserMailer:
describe "Signup Email" do include EmailSpec::Helpers include EmailSpec::Matchers # include ActionController::UrlWriter - old rails include Rails.application.routes.url_helpers before(:all) do @email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") end it "should be set to be delivered to the email passed in" do @email.should deliver_to("jojo@yahoo.com") end it "should contain the user's message in the mail body" do @email.should have_body_text(/Jojo Binks/) end it "should contain a link to the confirmation link" do @email.should have_body_text(/#{confirm_account_url}/) end it "should have the correct subject" do @email.should have_subject(/Account confirmation/) end endRSpec Matchers
reply_to(email)
alias: have_reply_to
This checks that the Reply-To header's email address (the bob@example.com of "Bob Saget bob@example.com") is set to the given string.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should reply_to("support@myapp.com")deliver_to(*email_addresses)
alias: be_delivered_to
This checks that the To header's email addresses (the bob@example.com of "Bob Saget bob@example.com") are set to the addresses.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should deliver_to("jojo@yahoo.com")deliver_from(email)
alias: be_delivered_from
This checks that the From header's email address (the bob@example.com of "Bob Saget bob@example.com") is set to the given string.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should deliver_from("sally@yahoo.com")bcc_to(*email_addresses)
This checks that the BCC header's email addresses (the bob@example.com of "Bob Saget bob@example.com") are set to the addresses.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should bcc_to("sue@yahoo.com", "bill@yahoo.com")cc_to(*email_addresses)
This checks that the CC header's email addresses (the bob@example.com of "Bob Saget bob@example.com") are set to the addresses.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should cc_to("sue@yahoo.com", "bill@yahoo.com")have_subject(subject)
This checks that the Subject header's value is set to the given subject.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should have_subject("Welcome!")include_email_with_subject(subject)
Note: subject can be either a String or a Regexp
This checks that one of the given emails' subjects includes the subject.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email2 = UserMailer.forgot_password("jojo@yahoo.com", "Jojo Binks") [email, email2].should include_email_with_subject("Welcome!")have_body_text(text)
Note: text can be either a String or a Regexp
This checks that the text of the body has the given body.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should have_body_text(/Hi Jojo Binks,/)have_header(key, value)
This checks that the expected key/value pair is in the headers of the email.
email = UserMailer.create_signup("jojo@yahoo.com", "Jojo Binks") email.should have_header("X-Campaign", "1234abc")Using the helpers when not testing in isolation
Don't. :) Seriously, if you do just take a look at the helpers and use them as you wish.
Original Authors
Ben Mabey, Aaron Gibralter, Mischa Fierer
Please see History.txt for upcoming changsets and other contributors.
Or: How I Learned to Stop Worrying and Love Testing Start Behaving
Introduction
This document deals principally with initial set up and first use of the Cucumber-Rails and Cucumber gems. It takes as its background the Ruby on Rails (RoR) web application framework. Discussion of Behaviour Driven (BDD), Test Driven (TDD), and Panic Driven Development (SNAFU aka Cowboy Coding) can be found elsewhere. Details regarding installing the Cucumber gem and its recommended support tools for RoR are found on this wiki under the heading Ruby on Rails. The article Getting Started with Rails is a useful introductory tutorial dealing with Rails and Cucumber.
Since the original version of this article appeared, Cucumber has undergone repeated revisions and re-factorings. Among these was the sensible decision to move portions of the implementation specific to particular frameworks into their own gems. Consequently, installing Cucumber for a framework now frequently starts with installing the specific framework Cucumber gem, which in turn pulls in the core Cucumber gem as a dependency. Cucumber now provides support for a wide range of Ruby VMs such as JRuby, alternative application frameworks such as Sinatra, other programming languages such as Python, test environments such as Capybara, and provides i18n language support for feature and step files. Obtaining these features may require the installation of other gems such as cucumber-sinatra.
Readers should always consider that the information contained herein may be out of date and therefore incomplete or erroneous in some respects. However, any such defects will usually be confined to specific implementation details and should not detract from the validity of the overall presentation.
Note that in this document I often use the terms testing and test where BDD practitioners prefer the terms behaviour and expectation. When I use the word test in a BDD context I am in fact discussing expressing and verifying expected behaviour.
Where to Start?
Feature: Design and Build a Ruby on Rails web app using Behaviour Driven Development (BDD) In order to reduce rework and produce a web app at low cost and high speed A developer Should employ a BDD methodology and agile tools Scenario: Cucumber should be installed and configured Given I have installed the gem named "rails" And I have installed the gem named "cucumber-rails" And I have generated a RoR project named "my_project" And the present working directory is the Rails root directory of "my_project" And I have the file cucumber.yml in the config directory of "my_project" And the file cucumber.yml contains a valid default profile When I run "rails g cucumber" Then I should create the directory ./features And I should create the file ./features/features.feature And I should create the directory ./features/step_definitions And I should create the file ./features/step_definitions/web_steps.rb And I should create the directory ./features/support And I should create the file ./features/support/env.rb And I should create the file ./features/support/paths.rb And I should create the file ./lib/tasks/cucumber.rake And I should create the file ./config/environments/environment cucumber.rb And I should create the file ./config/cucumber.yml And I should modify ./config/database.yml . . .The foregoing gives a sample of the form that feature files often take ( sadly ). The statements, called feature steps or feature statements, make up the user interface to Cucumber testing. Those given above are written in the Imperative Style simply for illustrative purposes. Never put statements that look anything like these in a feature file ( but you will ). In practice, all those Then/And statements should be subsumed into one simple Declarative Style statement. For example: I should create the Cucumber environment. The messy details of just what comprises a Cucumber environment are placed in the step definition files. Instead, a feature should look more like this:
Feature: Design and Build a Ruby on Rails web app using Behaviour Driven Development (BDD) In order to produce a web app at low cost and high speed A developer Should employ Ruby on Rails with Cucumber BDD tools Scenario: Cucumber-Rails should be installed and configured Given I am in a rails project root And I have installed cucumber-rails And I do not have a cucumber environment When I run the cucumber-rails generator Then I should have a cucumber environmentWe will return to how to write features and steps later. For the moment we deal with the logical arrangement of Cucumber files within the context of an RoR project. The root level of the archetypal RoR project directory tree looks like this:
MyProject |-- README |-- Rakefile |-- app |-- config |-- db |-- doc |-- lib |-- log |-- public |-- script |-- test |-- tmp `-- vendorDepending upon the version of Rails, running rails g cucumber or script/generate cucumber:install adds this layout to the existing structure:
Note Bene: Take the time to read the generator help first, rails g cucumber:install --help
Also note that the cucumber_rails gem at version 0.4.1 does not properly configure support/env.rb for use with testunit or capybara. Details on how to remedy this are on the issues page of cucumber-rails here at github.|-- features | |-- step_definitions ` | `-- web_steps.rb `-- support |-- env.rb |-- hll_cuke_sample_helpers.rb |-- paths.rb `-- selectors.rb
If you are not using Rails and Cucumber-Rails in your project then you can accomplish much the same thing by creating the directory tree from the command line (mkdir -p features/step_definitions) adding the support directory and simply ignoring all of the (Rails specific) files . Once the features directory structure is in place then we are ready to begin testing with Cucumber.Where do I put Tests?
Cucumber divides testing into two parts, the outward facing feature steps and the inward facing step definitions. Features are descriptions of desired outcomes (Then) following upon specific events (When) under predefined conditions (Given). They are typically used in conjunction with end-user input and, in some cases, may be entirely under end-user (in the form of a domain expert) control. Feature files are given the extension .feature.
Step definitions, or stepdefs, are keyed by their snippets of text from the feature scenario statements and invoke blocks of Ruby and Rails code that usually contain api methods and assertion statements from whatever test system you have installed (TestUnit, RSpec, Shoulda, etc.). Given that Cucumber evolved out of RSpec stories it is unsurprising that the Cucumber generator once assumed that RSpec was available. This has long since ceased to be the case. What Cucumber does now is detect if the RSpec gems are installed. If so then the rails generator builds the environment files to suit and if not then it ignores RSpec and configures for test-unit instead. In fact, the availability of Cucumber generator options is increasing over time. To see what is available in the version of Cucumber-Rails that you have installed use the command: rails g cucumber:install --help or script/generate cucumber --help.
A source of potential confusion is that the term steps, when used loosely, has two, closely related but vitally distinct, meanings depending on context. Inside feature.feature files, steps are the textual descriptions which form the body of a scenario. These are prefaced with the keywords Given, When, Then, And or But (note as well that the capitalization of these five names is significant). Inside a step_definitions.rb file, steps (which strictly speaking should always be called step definitions and are now often simply called stepdefs) refers to the matcher methods, which may be given any of the same names (Given, When, Then, And or But), each provided with a matcher regexp that corresponds to one or more feature steps. Note that the method name does NOT form part of the matcher. A Given feature clause can match a When step definition matcher. Over time, clauses from features have come to be referred to simply as features while steps now refers almost exclusively to step definitions.
As shown above, the generated features directory tree is fairly shallow. One can put every feature into a single file in the features directory and every step in a single file in the steps_definition directory (or even in the features directory itself) if one so chooses. Alternatively, one can choose to have one or more feature files for each feature, together with one or more step files for each feature file, or any combination thereof. However, Cucumber is programmed with the flexibility to support a much more expressive directory structure. For instance:
|-- features | |-- entities | | |-- entity.feature | | `-- step_definitions | | |-- anything.rb | | `-- entity_steps.rb | |-- locations | | |-- location.feature | | `-- step_definitions | | `-- location_steps.rb | |-- sites | | `-- step_definitions | |-- step_definitions | | |-- local_assert_steps.rb | | |-- local_crud_response_steps.rb | | |-- local_email_steps.rb | | |-- local_file_steps.rb | | |-- local_script_steps.rb | | |-- local_steps.rb | | |-- local_web_steps.rb | | |-- local_xml_file_steps.rb ` | `-- web_steps.rb `-- support |-- env.rb |-- hll_cuke_sample_helpers.rb |-- local_env.rb `-- paths.rbIn this case the bland initial set-up has been divided into sub-directories informed by model-centric testing. This could equally well have been broken up in to model/controller/view hierarchies:
|-- features | |-- models | | `-- entities | | |-- entity.feature | | `-- step_definitions | | |-- anything.rb | | `-- entity_steps.rb | |-- views | | |-- entity_new | | `-- step_definitions | | `-- entity_new_steps.rb | |-- step_definitions | | |-- local_steps.rb ` | `-- web_steps.rb `-- support |-- env.rb |-- hll_cuke_sample_helpers.rb |-- local_env.rb `-- paths.rbConsider, however, that the Cucumber feature scaffold generator will generate the files it produces in conformance with the default layout, with the manage_frooble.feature file placed in ./features and the frooble_steps.rb file in ./features/step_definitions. Further, despite the behaviour provided in the Cucumber-Rails generator, it is considered an anti-pattern to relate step_definition files to specific feature files. As is the case for many programming suggestions there are exceptions and contrary opinions respecting this advice. Nonetheless, it is probably best to follow this recommendation at the outset and depart from it only when you are experienced enough to evaluate the trade-offs between approaches.
Also be aware that, regardless of the directory structure employed, Cucumber effectively flattens the features directory tree when running tests. By this I mean that anything ending in .rb under the start point for a Cucumber feature run is searched for feature matches. Thus, a step contained in features/models/entities/step_definitions/anything.rb can be used in a feature file contained in features/views/entity_new, providing that cucumber is invoked on a root directory common to both, ./features in this case; or explicitly required on the command line, $ cucumber -r ./features features/views/entity_new. It is also worth noting that step files can be called anything so long as they end in .rb.
How do I Write Tests?
Constructing ones first tests, or features as BDD purists prefer, is often accompanied by what can only be described as writer’s block. The question of “Where to begin?” seems to be a particular roadblock for some. If you truly have no idea of where to start then I suggest that you consider what you are writing, presumably a web application, and what the initial point of contact between it and a user is, the home page. You can do worse than simply starting with:
Feature: An application to do whatever In order to generate revenue The users Should be able to visit our web site Scenario: The application has a home page Given I do have a web application When I visit the home page Then I should see the home page And I should not see a 404 pageFrom then on, the easiest thing to do for the first time tester/behaviourist is to use Cucumber’s built-in scaffold generator to create a feature scaffold for each new feature desired and then modify the resulting files to suit.
script/generate feature Frooble name color description exists features/step_definitions create features/manage_froobles.feature create features/step_definitions/frooble_steps.rbThis might be the easiest thing to do, but not necessarily the best thing to do. Generating scaffolds provides a comforting illusion of progress through voluminous production of boilerplate code. Said code is, for all intents and purposes, worthless, save only as an example of proper syntax – and even the syntax is sometimes dubious. By all means take a look at the generated code to obtain some idea of how things should look when you write your own features, but do not expect your project to design itself.
Instead of simply modifying boilerplate, it proves important to write each test/feature step one at a time and in plain language. Detailed discussion of feature writing and step construction are provided elsewhere (see Given-When-Then and Telling a Good Story).
The use of plain language in the feature file is crucial to successful use of Cucumber. What does plain language mean? Basically, it comes down to stating the result that you wish to obtain while avoiding specifying how you expect to get it. For example, for an authentication scenario you should write:
When "Bob" logs inand not:
Given I visit "/login" When I enter "Bob" in the "user name" field And I enter "tester" in the "password" field And I press the "login" button Then I should see the "welcome" pageWhat is important about the difference in styles? The first example, When “Bob” logs in, is a functional requirement. The second, much longer, example is a procedural reference. Procedures belong in the implementation details.
What you and your client should concern yourselves with in your feature files is that which has to happen and not how you expect it to happen. That way, when somebody later decides that challenge and response authentication schemes are passé then you simply need change the authentication process steps behind the scenes. Your outward facing feature files, the ones that your clients get to see, need not change at all. In fact, a good question to ask yourself when writing a feature clause is: Will this wording need to change if the implementation does? If the answer is yes then the clause is poorly written and you should rework it avoiding implementation specific details. As a side benefit, in consequence your scenarios will be a lot shorter and much easier to follow and understand.
After each new feature statement is added to its scenario then you should immediately create the corresponding step definition method. This is where the implementation details are put because, in the normal course of events, your users will never see them. Once your new step definition is written then you must prove to yourself that it fails by running it against the, as yet, non-existent application code. Then, and only then, should you write the least application code that gets your test/step definition to pass. Now that you have a passing step, without changing the step definition’s logic change the test criteria within it to something that cannot be and prove to yourself that it fails again. Once you have assured yourself that your test is passing for the right reason then reset the criteria so that the test passes again. Once this cycle is complete then move on to the next feature clause. For example:
Scenario: Users can enter an invoice item . . . Then I enter a product quantity of 5Now, immediately go to your step_definition file and do this:
When /enter a product quantity of (\d+)/ do |quantity| pending "TODO: Do we need to have a product code passed as well?" endThink about how you are going to express this behaviour in your application and how you can detect that it occurs. Go back and rework your feature and step until you are satisfied that it will indeed produce some testable result and that the test fails. Now, go write the code to implement this requirement in your application.
It is tempting, sometimes irresistibly so, to just skip ahead with the analysis stage alone and to complete as many features, scenarios and scenario statements as one can imagine. In some cases limited access to domain experts and end users may require that many features have their scenario details completed long before coding the associated step definitions is undertaken. When this is avoidable it should be and when it is not avoidable then every effort should be made to avoid it nonetheless. In the long run, the best results are achieved when you write feature statements and step definitions incrementally, using the absolute minimum of code, and then immediately implement the new step requirement in the application, also using the absolute minimum code that will satisfy it.
This is a hard discipline to accept but, the value with this approach is that you will rarely ( never ) have untested code anywhere in your application. More importantly, if you rigorously adhere to this methodology then your application will only contain code that satisfies required features. This is an often overlooked or undervalued consideration that contributes greatly to the efficiency of coding and the robustness of the resulting code. Avoiding diversions into technically interesting and financially pointless coding adventures concentrates resources on the tasks that count and reduces the overall complexity of the project. For if the user is not asking for it then why are we writing it?
Sticking with this approach you can face significant design changes ( and gem updates ) with complete equanimity, secure in the knowledge that if unanticipated changes break anything anywhere in your project then you will know of this immediately upon running your test suite. More importantly, you will know exactly what is broken and where it is broken. Like any skilled tradesman, most of the value programmers provide lies in discovering what piece of code to change and not simply how to change it. Finding the spots that need attention in an application is usually the biggest maintenance problem. Strictly coding to features simplifies that task to the point of triviality.
If it happens that, on occasion, you do anticipate feature steps ( and we all do this however much we try not to ) then omitting any matcher for them in the step definitions files causes these steps to be reported as missing by Cucumber. Not only does Cucumber report them, it helpfully provides a suggested step matcher and argument to implement. If you end up writing stub step matchers then you have an explicit pending method available to designate defined but pending/unspecified/stub step definitions, together with an optional message. These step definitions will display as defined but pending in your Cucumber runs.
Given /this step is not implemented yet/ do pending "your message goes here" endWhat are Features and Scenarios?
A feature can be conceptualized as an indivisible unit of functionality embedded in the project to which it belongs. For example, an authentication challenge and response user interface is usually considered a feature while an entire authentication system necessarily comprises many features. A single Feature is typically contained in its own file (ending in .feature). Each Feature usually consists of multiple Scenarios.
A Scenario is a block of statements that describe some behaviour desired or deprecated in the feature to which it belongs. A scenario might check that the login interface provides input fields for the requisite responses, that failures are logged or otherwise reported, that user ids are locked out after a given number of failed attempts, and so forth. Each scenario exercises the implementation code to prove that for each anticipated condition the expected behaviour is indeed produced. Recall that scenarios specify What and should avoid answering the question: How?
Each Scenario consists of three classes of statements, Given, When and Then which effectively divide each scenario into three stages. Each stage of a scenario consists of one or more statements that are used to match to test step definitions. The conventional arrangement is:
Feature: Some terse yet descriptive text of what is desired In order that some business value is realized An actor with some explicit system role Should obtain some beneficial outcome which furthers the goal To Increase Revenue | Reduce Costs | Protect Revenue (pick one) Scenario: Some determinable business situation Given some condition to meet And some other condition to meet When some action by the actor And some other action And yet another action Then some testable outcome is achieved And something else we can check happens too Scenario: A different situation ...For Cucumber features the key words used here are Feature, Scenario, Given, When, Then, and And. Feature is used to provide identification of the test group when results are reported.
At the present time the Feature statement and its descriptive text block are not used by Cucumber other than as an identifier and documentation. However, the Feature statement is actually the central piece of information contained in a feature file. It is here that you answer the question of just why this work is being done. And, if you do not have a very good, defensible, reason that can be clearly elucidated in a few sentences then you probably should not be expending any effort on this at all. First and foremost, BDD absolutely MUST have some concrete business value whose realization can be measured before you write a single line of code. (see popping the why? stack)
As with Feature, Scenario is used only to identify and document a piece of the work. The clauses that make up a Scenario begin with one of: Given, When, Then, And and But. These are all Gherkin keywords / Cucumber methods that take as their argument the string that follows. These are the steps that Cucumber will report as passing, failing or pending based on the results of the corresponding step matchers in the step_definitions.rb files. The five keywords are all equivalent to one another and completely interchangeable.
The string following the keyword is compared against all the matchers contained in all of the loaded step_definitions.rb files. A step definitions matcher looks much like this:
Given /there are (\d+) froobles/ do |n| Frooble.transaction do Frooble.destroy_all n.to_i.times do |n| Frooble.create! :name => "Frooble #{n}" end end endThe significant thing here is that the method (Given) takes as its argument a regexp bounded by /. Among other things, that means that all the arguments are received as string values. Thus n.to_i.times and not simply n.times. It also means that step matchers can be followed by the special regexp modifiers, like i if you want to avoid issues involving capitalization.
In the feature provided above we had the scenario statement: And Some Other Action. This could be matched by any of the following step definition matchers if present in any step_definitions.rb file found under the features root directory.
Given /some other action/ do When /some other Action/i do When /some other (Action)/i do |action| Then /(\w+) other action/i do |prefix_phrase| Given /(\w+) other (\w+)/i do |first_word,second_word| But /(\w+) Other (.*)/i do |first_word,second_phrase| And /(.*) other (.*)/i do |first_phrase,second_phrase|The step definition match depends only upon the pattern following the Given/When/Then method and not upon the step method itself. I have therefore adopted the practice of only using When /I have a match/ do in my step definitions files as When has a more natural appearance, to me, for a matcher.
If Cucumber finds more than one matcher in all of the step definitions files matches a scenario statement then it complains that it has found multiple step definition matches for that step and forces you to distinguish them. You can instruct Cucumber to just choose one of the candidates instead by passing it the --guess option on the command line.
It is considered better form by some to surround with double quotation marks, " ", all of the elements in the feature step clauses that are meant to be taken as values for variables passed to the step definition. This is just a convention. However, if you choose to follow this road then you must adjust your step definition matchers accordingly. For example:
Given some determinable "business" situationGiven /determinable "(.*)" situation/ do |s|Finally, you can have step definitions call other step definitions, including those contained in other step definitions files. This is where you can specify the procedural details by combining other steps. For example:
When /some "(.*)" action/ do |act| . . . end When /in an invoiced non-shipped situation/ do Given "some \"invoiced\" action" Then "some \"non-shipped\" action" . . . endSteps within Steps – an anti-pattern
If one step definition calls another step definition then the matcher argument to the called Given/When/Then method must be enclosed with string delimiters. Because of this, if you have adopted the practice of demarcating parameter values present in feature steps with double quotation marks, you must escape these quotation marks when calling another definition_steps.rb matcher from inside a step_definitions.rb file. You must take care not to include the quote marks in the step_definitions parameter matchers, for "(.*)" is not the same as (.*) or (".*"). If you use quote delimited values in the .feature file steps and do not account for them in the corresponding step_definition.rb matcher regexp then you will obtain variables that contain leading and trailing quotes as part of their value.
Scenario: Quotes surround value elements Given some "required" action # step_definitions When /some (.*) action/ do |a| a => "required" When /some "(.*)" action/ do |a| a => requiredIn a manner similar to my convention of using only using the When method for step definition matchers, as When, Given, and Then are all equivalent inside the step definitions file, I have adopted the convention of calling other step definitions within matchers using the Then method, thus:
When /my matcher named (.*)/ do |match| Then "my other matcher named \"#{match}\"" endThe choice of matcher method has no impact on the result but, When and Then used in this fashion seem to me to read more naturally. You should also realize that the argument passed to the Then method is a string and that the string may be constructed in any manner. For example the above Then statement could be passed as:
When /my matcher named (.*)/ do |match| Then %Q(my other matcher named "#{match}") endThis latter format removes the necessity to escape (\) the embedded quotation characters ("). Multiple steps may be called using the steps method which itself takes a string argument:
When /my matcher named (.*)/ do |match| steps %Q{ Then my other matcher named "#{match}" And the next matcher with value "{match}" } endKeep in mind that Cucumber is simply a DSL wrapper around the Ruby language, whose full expressiveness remains available to you in the step definition files. On the other hand, do not lose sight that every step called as such in a step definition file is first parsed by Gherkin and therefore must conform to the same syntax as used in feature files.
Returning to our example of “Bob” the user, one could define things in the step_definitions file like this:
When /"Bob" logs in/ do |user| steps( %Q( Then I visit "/login" And I enter "#{user}" in the "user name" field And I enter "#{user}-test-passwd" in the "password" field And I press the "login" button ) )That is acceptable ( barely ) usage in your step_definitions because your users are never going to see how ugly it looks. Instead, given that the necessary classes and methods exist, “Bob” could, and should, be authenticated without recourse to the user interface thus:
When /"Bob" logs in/ do |user| @current_user = User.find_by_username!(user) # ! method raises exception on failure @current_session = UserSession.create!(@current_user) # ! method raises exception on failure . . . endOf course, when you are testing the login user interface the ugly approach seems unavoidable, but in fact it is not. Providing for the purposes of testing that certain conventions are followed respecting user names and passwords the following works just as well and is much cleaner. Plus you have removed all inter-step dependencies.
feature statement:
When "Bob" logs on through the logon pagestep_definition:
When /"([\w[\d\w]+)" logs on through the logon page/ do |user_name| visit(logon_path) fill_in( "User Name", :with => user_name ) fill_in( "Password", :with => user_name + "-test-passwd" ) click_button( "Logon" ) endHaving just shown you how to call steps from within other steps take heed that you do not write many step definitions that are only called from other steps and are never found in feature files. Sometimes this will seem like the quickest solution to a troublesome bit of environment building. However, for anything extensive in nature it is nearly always better to implement a custom method using the api provided by Cucumber ( or by any other libraries you have installed ) and then call that method directly from your step. You can stick these custom methods in any convenient file ending in .rb that is located in the support directory ( well, anywhere that cucumber can find it really ) and which encloses your methods with the following block:
Cucumber::Rails::World.class_eval do def your_method(parm) . . . end endMy rule of thumb is that if a step definition requires more than eight to ten lines of code, and it is called from any other step definition, then its contents probably should be extracted out into a custom method.
Before, After and Background
If all your feature’s scenarios share the same ‘setup’ feature steps then Cucumber provides the Background section. Steps contained within a Background section are run before each of the scenarios.
Feature: . . . Background: . . . Scenario: . . .Step definition files have a corresponding method available in the before(condition) do . . . method, which has however a matching after(condition) do . . . method as well. Recall that we are working in Ruby and therefore the condition which enables the before/after block is anything that is not false or nil, like a tag for instance. Also be aware that all eligible before methods are run before any scenario statements are processed, and that they are run in the order encountered. Likewise, every eligible after block will run at the completion of every scenario, again in the order that it is encountered. These two methods are powerful tools, but be aware that if you use them excessively then you will hang yourself eventually.
What is a good Step Definition?
Opinions vary of course, but for me a good step definition has the following attributes:
- The matcher is short.
- The matcher handles both positive and negative (true and false) conditions.
- The matcher has at most two value parameters
- The parameter variables are clearly named
- The body is less than ten lines of code
- The body does not call other steps
My template for a step definition presently looks like this:
When /statement identifier( not)? expectation "([^\"]+)"/i do |boolean, value| actual = expectation( value ) expected = !boolean message = "expectation failed for #{value}" ... endFor example ( admittedly contrived ):
When /product ([^\"]+) should( not)? belong to category ([^\"]+)/i do |product, boolean, category| actual = ( Product.find_by_stock_number!( product ).category ) == category expected = !boolean message = "Product '#{product}' should#{boolean} belong to category '#{category}'" assert( actual == expected, message ) endWhat are “tags”?
Cucumber provides a simple method to organize features and scenarios by user determined classifications. This is implemented using the convention that any space delimited string found in a feature file that is prefaced with the commercial at (@) symbol is considered a tag. As distributed, Cucumber-Rails builds a Rake task that recognizes the @wip tag. However, any string may used as a tag and any scenario or entire feature can have multiple tags associated with it. For example:
@init Feature: . . . . . . @wip @authent Scenario: A user should authenticate before accessing any resource. Given I do have a user named "testuser" When the user visits the login page And the user named "testuser" authenticates successfully Then I should see . . . . . .Given that the forgoing is contained in a file called features/login/login.feature and that the cucumber-rails gem is installed and configured then you can exercise this scenario, along with any others that are similarly tagged, in any of the following ways:
$ rake cucumber:wip $ cucumber --profile=my_profile --tags=@wip features $ cucumber --profile=my_profile --tags=@authent features/login $ cucumber --profile=my_profile --tags=@initHowever, the following will not work, unless you build a custom rake task for it:
$ rake cucumber:authentThere is an obscure gotcha with this particular combination of tags. The default profile contained in the distributed config/cucumber.yml contains these lines:
<% . . . std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} --strict --tags ~@wip" %> default: <%= std_opts %> features . . .Note the trailing option --tags ~@wip. Cucumber provides for negating tags by prefacing the --tags argument with a tilde character(~). This tells Cucumber to not process features and scenarios so tagged. If you do not specify a different profile ( cucumber -p profilename )then the default profile will be used. If the default profile is used then the --tags ~@wip will cause Cucumber to skip any scenario that is so tagged. This will override the --tags=@authen option passed in the command line and so you will see this:
$ cucumber --tags=@authent Using the default profile... 0 scenarios 0 steps 0m0.000sYou can overcome this by adding the --tags=@wip to the Cucumber argument list on the command line. This overrides the default setting in the configuration file. Note however that @wip tags are a special case. If any scenario tagged as @wip passes all of its steps without error then Cucumber reports this as a Scenario failure. Work in progress is not supposed to pass.
The number of occurrences of a particular tag in your feature set may be controlled by appending a colon followed by a number to the end of the tag name passed to the tags option, as in $ cucumber --tags=@wip:3 features/log*. The existence of more than the specified number of occurrences of that tag in all the features that are exercised during a particular cucumber run will produce a warning message. If the --strict option is passed as well, as is the case with the default profile, then instead of a warning the run will fail.
Limiting the number of occurrences is commonly used in conjunction with the @wip tag to restrict the number of unspecified scenarios to manageable levels. Those following Kanban or Lean Software Development based methodologies will find this facility invaluable.
As outlined above, tags may be negated by prefacing the tag with the tilde (~) symbol. In other words, you can exclude all scenarios that have a particular tag. For example, the following will only exercise all scenarios found in the directory tree rooted at features/wip that do not have the tag @ignore:
$ cucumber --require=features --tag=~@ignore features/wipA convention that I have adopted is tagging all scenarios created to track down a specific defect with tags of the form @issue_### where ### is the issue number assigned to the defect. This both handles multiple related scenarios and provides a convenient and self-documenting way to verify with cucumber that a specific defect either has been completely resolved or that a regression has occurred.
What Way do I Run the Tests?
Unless you are knowledgeable enough that you can use mocks and stubs with flair then I consider it best to begin with creating a Rails migration file for the models you are testing ( or expressing features for ) followed by:
rake db:migrate rake db:test:prepareAs this is opinionated software my opinion is that, except for the most trivial of cases, you should always use test data obtained from actual production environments. You are, after all, embarked on a real-world adventure; namely to discover how to make something work. However, to discover what actually works requires more than a passing familiarity with what is real. And made-up data is not reality. Since your manufactured data originates in the same place as most of your errors will, your own limited understanding of the problem domain, it is always suspect.
That said, there remains an important environmental consideration to keep in mind when using an actual database for testing: Cucumber, by default, uses database transactions and these transactions are rolled back after each scenario. This makes out-of-process testing problematic (for that see the Cucumber Aruba project) and may result in some unanticipated outcomes under certain scenarios. Transactions can be turned off, but then your features become responsible for ensuring that the database is in a condition suitable for testing. Cucumber provides hooks to accomplish this and the gem Database-Cleaner is configured in support/env.rb to assist. In the normal case, the end of any scenario results in the database being returned to a nil state.
Cucumber can be run in several ways. Be aware that rake cucumber, cucumber features, and autotest with ENV AUTOFEATURE=true do not necessarily produce the same results given the same features and step definitions.
Running rake cucumber from the command line provides the simplest, if not the speediest, method to run Cucumber tests. The rake script provided with cucumber performs much of the background magic required to get the test database and requisite libraries properly loaded. In fact, an important habit to acquire is to run cucumber as a rake task immediately after performing a migration. This step ensures that the test database schema is kept in sync with the development database schema. You can achieve the same effect by running rake db:test:prepare before your first cucumber run following a migration but developing the habit of just running rake cucumber or rake cucumber:wip is probably the better course.
As discussed above, the Cucumber Rake task recognises the @wip tag, so rake cucumber:wip will run only those scenarios tagged with @wip. For example, given a feature file containing:
Feature: . . . Scenario: A @wip Scenario: B Scenario: CThen running the command rake cucumber:wip will run the steps contained inside Scenario B only, while running rake cucumber:ok will run the steps within all Scenarios other than B.
Cucumber-Rails creates a cucumber.yml file in the project config directory containing a number of predefined profiles, one of which is the default profile. When Cucumber is run from the command line it is usually necessary to provide both the directory name containing the root directory of the tree containing feature files and the directory name containing references to the necessary library files. In the typical project cucumber -r features features/some/path will normally suffice. Repetitious usages can be added to user defined profiles contained in the project’s cucumber.yml file.
Finally, running autotest with the environment variable AUTOFEATURE=true will run ALL tests, including those in /test and (if present) /rspec. As this will load all the TestUnit and RSpec fixtures as well, your test database may be left in an indefinite state when the Cucumber features are run. It is wise, as always, to write Cucumber steps either so that they do not depend upon an empty database or they place the database in the requisite state.
Anything Else?
The terminology for elements of Behaviour Driven Development differs somewhat from that employed by Test Driven Development. This article, because of the introductory nature of its contents, tends to blur the semantic distinction between these two divergent philosophies.
Cucumber is evolving, rapidly. Originally, Cucumber was written for Ruby on Rails. As discussed above this has long since ceased to be true and Cucumber now has a supplementary library, Aruba, which permits testing of Command Line Interface processes. This article is revised to Cucumber version 1.0.0 and Cucumber-Rails version 0.4.1 but it does not cover many of the ever expanding attributes of Cucumber, Cucumber-Rails, Aruba, and only mentions the JVM version, Cuke4Duke, here.
Because of this consideration it would not be wise to use any of the examples from this article as a recipe. Nonetheless, the essentials of this article remain applicable throughout all recent versions of Cucumber and Cucumber-Rails even where the implementation details may have changed since this review.
Cucumber supports tables in feature files. These are roughly analogous to Framework for Integrated Test (FIT) tables. You can use these when you are specifying behaviour that changes at some data threshold or as a substitute for data fixtures. I tend to avoid using tables in feature files altogether and use them sparingly in step definition files. I do not have any explicit reason for this avoidance but, tables and feature statements just do not seem to go together in my head.
If you are testing with intent then you should be using the ruby-debug gem (ruby-debug19 for Ruby-1.9.2+ [Do NOT use 1.9.1 with Rails]). A really neat method to drop into an interactive debugging session inside a Cucumber step definition using ruby-debug was provided by Scott Taylor on the rspec mailing list. Just put these statements inside the step definition at the point that you wish to debug: require 'rubygems'; require 'ruby-debug'; debugger. When that code interrupts then type irb and you open an interactive debugging session wherein you can step forwards and backwards inside the code under test to determine exactly where the breakage is happening. Alternatively, you can add require 'rubygems'; require 'ruby-debug'; to your support/local_env.rb file (see below) and just put debugger wherever you desire it inside any step definition.
Realize that tests/assertions/expectations either pass or fail (raise an error) and that fail is NOT the same as false, whereas anything but fail is a pass. When, in RSpec, something.should_be 0 and it is not, then what is returned is an error exception and not a Boolean value. In raw Cucumber (pardon the pun) one writes fail if false and not simply false. A little reflection reveals why this is so, since false might be the expected successful outcome of a test and thus not an error. However, this distinction between fail and false escaped my notice until I tripped over it in an actual test suit.
Recall that Cucumber is an integration test harness. It is designed to exercise the entire application stack from view down to the database. It is certainly possible to write features and construct step definitions in such a fashion as to conduct unit tests and no great harm will result. However, this is not considered best practice by many and may prove insupportable for very large projects due to the system overhead it causes and the commensurate delay in obtaining test results.
Cucumber-Rails is pre-configured with support for view integration testing using Capybara (script/generate cucumber —capybara). As of v0.5.0 support for Webrat (script/generate cucumber) has been dropped. If you do not know what Capybara or Webrat are or do, and you are doing web application testing, then you should find out more about both. If instructed using the —capybara option the Cucumber-Rails generator will set up the necessary support files. After some delay in release the most recent release of Webrat (0.7.3) evidently works with Rails-3.0.×. However, since the release of Rails 3.0, Capybara has apparently captured sufficient mind-share in the Cucumber community that generator support for Webrat was dropped.
While Capybara has emerged as the preferred testing method for html views it does not play well with Rails’ own built-in Test::Unit. In particular, whenever Capybara is required into a Cucumber World then the response.body method of Rails Test::Unit is removed. This is an annoyance more than anything else but people converting from Webrat need to be aware of it.
Calls for any additional Ruby libraries that your particular testing environment may require are typically placed in the ./features/support/local_env.rb file. I advise against putting local customization in support/env.rb as it is typically overwritten by script/generate cucumber:install | rails g cucumber. As a matter of good practice you should always run script/generate cucumber | rails g cucumber:install whenever you install an updated version of cucumber-rails. Unfortunately, there are some configuration options that simply must go into env.rb to have effect. So, check in your env.rb along with the rest of your version controlled files.
Those of you that have used growl or snarl to provide desktop notifiers from autotest are advised that, as of this writing, Cucumber did not hook into the :red :green notifier capability of autotest; so, no pop-ups when a step fails. However, there exists a project to add a similar functionality to Cucumber. See Cucumber_Growler.
autotest is installed via the ZenTest gem. If you use autotest then take a look at the contents of example_dot_autotest.rb in the ZenTest gem root directory.
Note Respecting cucumber-rails v0.5.0. (2011 June 28)
It seems that the latest versions of cucumber and cucumber-rails are oriented towards Ruby-1.9.2. If you are using Ruby-1.8.7 and you are employing testunit or Capybara then one or both of the following must be added to the support/env.rb file produced by the rails generator:
# for testunit in Ruby-1.8.7 ENV["RAILS_ENV"] = "test" if RUBY_VERSION =~ /1.8/ require 'test/unit/testresult' Test::Unit.run = true end
# for capybara require "capybara"This requirement may no longer be case following the recent release of cucumber-rails v0.5.1 but I am unable to check this as yet.
Need Help?
The best place to go for help, that I know of, is the Google Cucumber Group.
If you find a bug in Cucumber, or wish a new feature added, then you should open a ticket at Lighthouse for it.
As Ruby on Rails has recently moved issue tracking to GitHub it is possible Cucumber will follow suit in the near future. If the above link is dead try GitHub instead.
2008 November 28 – J. B. Byrne initial
2010 January 17 – J. B. Byrne revised to 0.6.1
2010 May 28 – J. B. Byrne revised 0.7.3
2010 July 13 – J. B. Byrne revised to 0.8.4
2010 October 11 – J. B. Byrne revised to 0.9.0
2010 November 06 – J. B. Byrne revised to 0.9.4
2011 March 29 – J. B. Byrne revised to 0.10.0
2011 May 13 – J. B. Byrne revised to 0.10.2
2011 June 28 – J. B. Byrne revised to 1.0.0Postscript.
A caution, Cucumber is meant to facilitate expressing required behaviours. Indirection and excessive adherence to the principle of DRY, particularly in features, is at variance with the intent and defeats the major benefit of the tool. Requirement expression in features should remain as self evident to the non-technical reader and be as self contained as possible. Resist the temptation to program the features themselves using esoteric aspects of the DSL. Features should remain patent statements of intent. If you feel the need to “program” a scenario in order to simplify writing a feature then you are likely doing something considerably at odds with the fundamental intent of BDD methodology. In such circumstances, mentally step back and reconsider your approach to the problem.
Rcov measures how much of your code is covered by tests.
RSpec, Cucumber, Webrat, RCov and Autotest are a powerful combination of tools for testing your Rails app. Unfortunately getting them to all work nicely together can be a bit of challenge. I recently configured a development environment from scratch on OS X 10.5 Leopard and kept track of all of the little details.
Prerequisites
I’m assuming you’ve got the following installed:
- ruby
- ruby gems 1.3.1
- Apple development tools
- git
- rails >= 2.3.2
- You’ve added github to your gem sources (gem sources -a http://gems.github.com)
RSpec & RSpec-Rails
First let’s grab the rspec1 and rspec-rails2 gems.
sudo gem install rspec-railsCucumber
Next we’ll install the cucumber3 gem
sudo gem install cucumberWebrat
Webrat4 is used by cucumber to simulate a browser for your integration tests. Webrat will also install nokogiri5.
RCov
I thought RCov6 would get installed with RSpec, but it wasn’t for me. You might not need to do this, but just to make sure…
Autotest
Autotest7 comes from ZenTest8 and allows you to have a kick ass workflow where you are constantly running relevant tests and less-constantly automatically running your entire test suite.
Optionally, Thoughtbot’s Factory Girl
Factory girl9 is a really helpful fixture replacement (and more) gem to use in conjunction with cucumber, checkout their much better explanation
sudo gem install thoughtbot-factory_girl --source http://gems.github.comOptionally, Carlos Brando’s Autotest Notification
While autotest normally runs in a terminal window, it can be setup to hook into applications like growl or snarl. The Autotest Notification9 gem helps make this setup a lot easier.
You will need growl installed and configured for this step the installation instructions on this gems github page are very easy to follow.
sudo gem install carlosbrando-autotest-notification --source=http://gems.github.comNext you need to turn autotest notifications “on”
A Sample Rails App
Let’s create a sample rails app for the rest of this guide.
Configuring Environment Variables
Autotest relies on some environment variables to run all of your features and specs correctly. If autotest “hangs” after you try to run it, or it just never seems to be watching your specs or features, this will most likely solve your problem.
Open the test.rb environment definition file in sample-app/config/environments/test.rb and add the following.
1 2 ENV['AUTOFEATURE'] = "true" ENV['RSPEC'] = "true"These lines will test autotest to run, and look for changes to, your specs (rather than test unit tests) and your cucumber features.
Update
If you don’t want to add these environment variables to every rails project you’ve got on your machine, you can also choose to set them as environment variables in your .bash_profile or .bashrc (or whatever shell you’re using) files.
export AUTOFEATURE=true export RSPEC=trueUnpacking Gems
Next let’s freeze (unpack) some gems that we’ll be using in our app. I’ve run into problems trying to use the system gems with cucumber, rspec and webrat, especially when I have multiple versions of any of them installed. Unpacking them into my rails app solves this problem for me.
mkdir sample-app/vendor/gems cd sample-app/vendor/gems gem unpack rails gem unpack rspec gem unpack rspec-rails gem unpack cucumberBecause webrat (and nokogiri) are native gems, that is, they are built locally on your machine based on its architecture, we won’t unpack those.
config.gem support
The current accepted practice, when using rails 2.3, and as suggested by the rspec guy(s) is to use rails’ config.gem functionality.Open sample-app/config/environments/test.rb and add the following lines:
config.gem "rspec", :lib => false, :version => ">= 1.2.0" config.gem "rspec-rails", :lib => false, :version => ">= 1.2.0" config.gem "cucumber", :lib => false, :version => ">= 0.2.3" config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com" config.gem "webrat", :lib => false, :version => ">= 0.4.3" config.gem "nokogiri", :lib => false, :version => ">= 1.2.3"Your version numbers may be different, but these are all current at the time of writing.
Boot Strapping RSpec and Cucumber
Before you can get very far with rspec or cucumber you need to run the bootstrapping scripts to give yourself the default files and directories.
# From inside your rails app sample-app/ script/generate rspec script/generate cucumberFactories
Depending on where you’re going to use your factories the most, you might want to save your file in either spec/ or features/. I chose the latter. Only complete this step if you plan to use the FactoryGirl gem.touch sample-app/features/factories.rbGetting Accurate RCov Data
By default RCov is setup to only use your specs when calculating code coverage. If you’re using Cucumber and RSpec, you’ll obviously want to include both types of tests to calculate your project’s true code coverage.
I picked up this rcov rake task from my co-worker Jay McGavren it does all of the heavy lifting for you, we’ll just need to make a couple of changes.
Drop this file into sample-app/lib/tasks/rcov.rake and use it by calling rake rcov:all from your terminal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26require 'cucumber/rake/task' #I have to add this require 'spec/rake/spectask' namespace :rcov do Cucumber::Rake::Task.new(:cucumber) do |t| t.rcov = true t.rcov_opts = %w{--rails --exclude osx/objc,gems/,spec/,features/ --aggregate coverage.data} t.rcov_opts << %[-o "coverage"] end Spec::Rake::SpecTask.new(:rspec) do |t| t.spec_opts = ['--options', ""#{RAILS_ROOT}/spec/spec.opts""] t.spec_files = FileList['spec/**/*_spec.rb'] t.rcov = true t.rcov_opts = lambda do IO.readlines("#{RAILS_ROOT}/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten end end desc "Run both specs and features to generate aggregated coverage" task :all do |t| rm "coverage.data" if File.exist?("coverage.data") Rake::Task["rcov:cucumber"].invoke Rake::Task["rcov:rspec"].invoke end endThe important part here is on line 7, we want rcov to exclude our features directory. We obviously don’t need or want rcov telling us that our feature files are not “covered”. To solve this problem we’ve simply excluded the features directory from rcov’s processing.
We also need to slightly modify sample-app/spec/rcov.opts to get the full rspec + cucumber coverage data.
Your rcov.opts should look like this:
--exclude "spec/*,gems/*,features/*" --rails --aggregate "coverage.data"We again want to ignore our cucumber features and we also want to tell rcov to aggregate data in a file called coverage.data. This is used in the above rake task.
Write Some Specs and Features!
Act like you know what you’re doing and write some models, controllers whatever. Add some specs and features too.
Autotest Workflow
Open a terminal and make your way to your sample rails app and fire up autotest. You might see something like the following, depending on how many specs and features you’ve got.
$> autotest loading autotest/cucumber_rails_rspec opts ... Finished in 0.06276 seconds 3 examples, 0 failures ================================================================================ /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/cucumber-0.2.3/bin/cucumber --format progress --format rerun --out /var/folders/Aq/Aqp06i3dFnqse+tQgQA+1++++TI/-Tmp-/autotest-cucumber.75956.0 features ................. 4 scenarios 17 passed steps /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/rspec-1.2.2/bin/spec --autospec spec/models/intern_spec.rb -O spec/spec.opts ... Finished in 0.062995 seconds 3 examples, 0 failures ================================================================================ /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/cucumber-0.2.3/bin/cucumber --format progress --format rerun --out /var/folders/Aq/Aqp06i3dFnqse+tQgQA+1++++TI/-Tmp-/autotest-cucumber.75956.1 features ................. 4 scenarios 17 passed stepsThe REALLY important stuff
- make sure you’ve got “ENV['AUTOFEATURE'] = true” in your test.rb otherwise autotest won’t run your features automatically
- make sure you’ve got “ENV['RSPEC'] = true” in your bash profile or else autotest won’t run your specs automatically
- make sure you’ve got “–aggregate = ‘coverage.data’” in your spec/rcov.opts file if you’re going to use the above rake task and hope to get combined rcov coverage data between rspec and cucumber
- make sure you’re excluding the features directory from rcov where required or else you’ll end up with misleading rcov data.
Gem Versions
Here’s a list of the current gems and their versions that I used in preparing this guide.
*** LOCAL GEMS *** actionmailer (2.3.2, 1.3.6, 1.3.3) actionpack (2.3.2, 1.13.6, 1.13.3) actionwebservice (1.2.6, 1.2.3) activerecord (2.3.2, 1.15.6, 1.15.3) activeresource (2.3.2) activesupport (2.3.2, 1.4.4, 1.4.2) acts_as_ferret (0.4.1) addressable (2.0.2) builder (2.1.2) capistrano (2.0.0) carlosbrando-autotest-notification (1.9.1) cgi_multipart_eof_fix (2.5.0, 2.2) cucumber (0.2.3) daemons (1.0.9, 1.0.7) data_objects (0.9.11) diff-lcs (1.1.2) dnssd (0.6.0) extlib (0.9.11) fastthread (1.0.1, 1.0) fcgi (0.8.7) ferret (0.11.4) gem_plugin (0.2.3, 0.2.2) highline (1.2.9) hpricot (0.6) libxml-ruby (0.3.8.4) mongrel (1.1.4, 1.0.1) mysql (2.7) needle (1.3.0) net-sftp (1.1.0) net-ssh (1.1.2) nokogiri (1.2.3) polyglot (0.2.5) rack (0.9.1) rails (2.3.2, 1.2.6, 1.2.3) rake (0.8.4, 0.7.3) rcov (0.8.1.2.0) RedCloth (3.0.4) rspec (1.2.2) rspec-rails (1.2.2) ruby-openid (1.1.4) ruby-yadis (0.3.4) rubynode (0.1.3) sources (0.0.1) sqlite3-ruby (1.2.1) term-ansicolor (1.0.3) termios (0.9.4) textmate (0.9.2) thor (0.9.9) thoughtbot-factory_girl (1.2.0) treetop (1.2.5) webrat (0.4.3) ZenTest (4.0.0)El Fin
Hopefully this guide was useful or had that one little step that you needed to get everything working. I’m sure this will all be out of date in the coming weeks, but I’ll try to keep it as up-to-date as possible. If you see any errors, or can better explain some of the missing pieces, please post a comment. Thanks!
1http://github.com/dchelimsky/rspec/tree/master
2http://github.com/dchelimsky/rspec-rails/tree/master
3http://github.com/aslakhellesoy/cucumber/tree/master
4http://wiki.github.com/brynary/webrat
5http://github.com/tenderlove/nokogiri/tree/master
6http://rubyforge.org/projects/rcov/
7http://www.zenspider.com/ZSS/Products/ZenTest/#rsn
8http://www.zenspider.com/ZSS/Products/ZenTest/
9http://github.com/thoughtbot/factory_girl/tree/master
10http://github.com/carlosbrando/autotest-notification/tree/master
Updates
2009-12-08 – Removed “sudo” when describing how to unpack gems (h/t xdotcommer)
Cucumber + Delayed Job
Using delayed_job with Cucumber
2010-09-1Delayed_job is a great Ruby solution for executing jobs asynchronously. It is intended to be run in the background, dispatching jobs that are persisted in a table . If you are using Cucumber you have to consider how the dispatching process is launched when your features are executed.
My first attempt after googling for this question was to create a custom Cucumber step that launched the execution of the jobs.
Given /^Jobs are being dispatched$/ do
Delayed::Worker.new.work_off
endIn this approach we have an specific Cucumber step for indicating when do we want to dispatch jobs. This step will be executed synchronously in the same Cucumber thread, so you have to invoke it after some step has introduced a new job in the queue and before the verification steps:
When I perform some action (that makes the server to create a new job)
And Jobs are being dispatched
Then I should see the expected resultsI think this approach is not very convenient:
Cucumber is intended to be used for writing integration tests. Tests that describe your application from the point of view of its users. Ideally, they should only manipulate the application inputs and verify its outputs through the UI. A user of your application will never need to know you are using a job dispatcher in your server.
While controlling the exact (and synchronous) execution of jobs makes writing tests easier, it doesn’t represent the temporal randomness which is in the very nature of an asynchronous job dispatcher. In my opinion, it is good that Cucumber features verify that this randomness is correctly handled (in some controlled limits).
I think a better approach is launching the jobs in the background, simulating the normal execution environment of your application. The idea is very simple: the job worker is started before each Cucumber scenario and is stopped after it. Cucumber tags represent a good choice for implementing these hooks. In this way, you can easily activate delayed_job only for the scenarios that need it.
When implementing this approach, I found a lot of problems for providing a proper RAILS_ENV=cucumber to the delayed_job command. In fact, I wasn’t able to make it work using launching the command script/delayed_job start from a Cucumber step. RAILS_ENV was simply ignored. What I finally did was executing the rake task directly.
Before('@background-jobs') do
system "/usr/bin/env RAILS_ENV=cucumber rake jobs:work &"
endFor stopping the jobs I had the same RAILS_ENV issue using script/delayed_job stop. I ended up killing the job processes using a parametrized kill command.
After('@background-jobs') do
system "ps -ef | grep 'rake jobs:work' | grep -v grep | awk '{print $2}' | xargs kill -9"
endUsing this approach you can get rid of specific steps for delayed_job. Instead, you just have to tag with @background-jobs the features/scenarios that needed it.
As a conclusion, I think that using background jobs in Cucumber is a better approach in general terms. I would only use the synchronous work_off approach for special cases.
Some notes on how to un-monkeypatch. Was useful for cleaning up after each test.
I'm trying to unit test a piece of code that I've written in Ruby that calls File.open. To mock it out, I monkeypatched File.open to the following:
class File def self.open(name, &block) if name.include?("retval") return "0\n" else return "1\n" end end endThe problem is that I'm using rcov to run this whole thing since it uses File.open to write code coverage information, it gets the monkeypatched version instead of the real one. How can I un-monkeypatch this method to revert it to it's original method? I've tried messing around with alias, but to no avail so far.
Initializing the test database before a Cucumber session (in Rails)In one of my side projects, I needed some setup data to be present in the test database before each Cucumber session. Since this took me a few moments to get right, I figured I’d document it here.
The goal
Cucumber will (quite naturally) start each scenario with a known database state: usually an empty database with a defined schema. That is quite annoying when your application relies on information stored in the database (e.g. application settings) to function properly.
To be clear, I’m talking about settings that will be loaded during the application “installation” phase (e.g. via a Rake task), not settings that users can set to determine their profile preferences (or similar).
Loading data before the Cucumber session
We’ll create a hook to load the data. Cucumber provides a set of hooks related to the Cucumber lifecycle (such as Before, After, AfterStep, AfterConfiguration). Since all we need to do is to run a bunch of code at the start of a Cucumber session (as opposed to running said code before/after each scenario), we don’t really need to use a Cucumber hook, but I’ll do so anyway in the interest of conveying meaning.
Cucumber will load all files conforming to the features/**/*.rb glob, so the hook code could technically be put anywhere, but for the sake of maintainability, we’ll put it in features/support/hooks.rb.
AfterConfiguration do # use a run-once hook to initialize test database with settings yaml = File.join(Rails.root, "/config/settings.yml") begin settings = YAML.load_file(yaml) rescue Exception => e puts "Couldn't load #{yaml} configuration file." p e.message p e.backtrace exit end settings.keys.each do |key| Setting.create(:name => key.to_s, :default_value => Base64.encode64(Marshal.dump(settings[key]))) end endAs you can tell, what we do is quite straightforward: we simply load the application settings and put them in the database.
Preventing destruction across scenarios
Since the default DatabaseCleaner strategy configured in features/support/env.rb is `transaction`, the settings stored in the database will persist across tests. But if you wanted to use the truncation strategy instead, you can ensure the settings table is kept by specifying
DatabaseCleaner.strategy = :truncation, {:except => %w[settings]}in features/support/env.rb (you’ll have to replace the setting that is already there).
After (My) RSpec best practices and tips, I’m happy to share my Cucumber best practices and tips!
This article will help you organize, clarify and reduce the size of your cucumber scenarios.
1. Organize your garden
Keep your feature files organized by grouping them by business object, then action and context if any. I put all the feature files in the same directory. For instance:
bank_account_add.feature bank_account_delete.feature user_signup.feature user_signup_when_invited.feature user_login.featureThe steps specific to the application should be organized by business object as well (bank_account_steps.rb, user_steps.rb…). Keep the file organized grouping the steps by Given / When / Then.
Do not overload the files generated by Cucumber like step_definitions/web_steps.rb and support/env.rb with your own steps, helpers or setup code. These files are likely to get overwritten when you update Cucumber so store your stuff in your own files.
2. Custom steps make your scenario DRY and accessible
Scenarios should have the same lifecyle as your code: Red, Green, Refactor to make them DRY and easy to read.
Group multiple steps together. For instance:
Given I follow "Send money" When I fill in "Email" with "mukmuk@example.com" And I fill in "Amount" with "10" And I select "Bank account" from "Source" And I press "Send" Then I should see "You've sent $10 to mukmuk@example.com"… could be refactored to:
Given I send "$10" to "mukmuk@example.com" from my "Bank account"This step definition is the following:
Given %{I send "$amount" to "$email" from my "$source"} do |amount, email, source| Given %{I follow "Send money"} When %{I fill in "Email" with "#{email}"} And %{I fill in "Amount" with "#{amount.delete('$')}"} And %{I select "#{source}" from "Source"} And %{I press "Send"} Then %{I should see "You've sent $#{amount} to #{email}"} endThis step can then be easily reused in other scenario keeping your features DRY. It also decouples the scenario from the UI so that you won’t have to change dozens of feature files when the UX guru changes translations or user flows.
3. Background: setup the DRY way
Make the feature focus on one business object/action/context and the background will get longer than the scenarios.
Feature: A user can cancel a transaction unless it's claimed by the recipient Background: Given I am logged in And I send "$10" to "mukmuk@example.com" from my "Bank account" Scenario: I can cancel as long as the payment is not claimed When I cancel my latest transaction Then I should see a cancellation confirmation Scenario: I can't cancel once the payment is claimed Given "Mukmuk" claimed the latest transaction Then I can't cancel my latest transaction4. Scenario outlines: scenario with variables!
A scenario outline contains variables allowing you to test multiple context using a truth table. For instance I use them to make sure that validation errors are displayed properly:
Scenario Outline: Add invalid bank account displays inline errors Given I follow "Add Bank Account" When I fill in "<field>" with "<value>" And I press "Add Bank Account" And I should see the inline error "<error>" for "<field>" Examples: | field | value | error | | Account | | Can't be blank | | Account | Sixty five | Should be 1 to 12 digits | | Account | 1234567890123 | Should be 1 to 12 digits |5. Multi-line step arguments: give your step a table for lunch!
A step can take a multi-line table as an argument. This is a great way to load up a bunch of data or to test the rendering of lists and tables. For instance:
Given I sent "$25" to "mukmuk@example.com" from my "Bank account" Then I should see the following transaction history: | create | complete | | deposit | in_progress | | transfer | pending | | withdrawal | pending |The step definition looks like the following:
Then "I should see the following transaction history:" do |table| table.raw.each do |event, state| page.should have_css("tr.#{event}.#{state}") end endI hope that these tips will help you growing healthy cucumber features!
Want more? Check out the Cucumber wiki, 15 Expert Tips for Using Cucumber by Dave Astels, You’re Cuking It Wrong by Elabs and You’re almost cuking it… by Antony Marcano. You could also like (My) RSpec best practices and tips. :)
Happy BDD!
Thanks to Jean-Michel Garnier for reviewing this article.
Opinions on cucumber seem to be divided in the Ruby community. Here at Elabs we’ve been using cucumber to fantastic success on all of our projects for more than a year. At the same time Steak and projects like it seem to be gaining traction; some people are seemingly frustrated and fed up with cucumber.
So where does this gulf of experiences come from, why is cucumber loved by some and hated by others. At the risk of over-generalisation and mischaracterisation I recently came up with a theory: the cucumber detractors are not using cuke the way it was intended.
This is in fact not their fault. The entire cucumber ecosystem, and in fact even cucumber itself, encourage its misuse.
A while ago someone created an issue on the Capybara issue tracker. The interesting thing about this issue wasn’t the problem itself, but rather the cucumber feature that the author presented in order to replicate the problem. This is the feature the author submitted:
Scenario: Adding a subpage Given I am logged in Given a microsite with a Home page When I click the Add Subpage button And I fill in "Gallery" for "Title" within "#document_form_container" And I press "Ok" within ".ui-dialog-buttonpane" Then I should see /Gallery/ within "#documents"At first glance this seems reasonable. But contrast this with the following, improved version:
Scenario: Adding a subpage Given I am logged in Given a microsite with a home page When I press "Add subpage" And I fill in "Title" with "Gallery" And I press "Ok" Then I should see a document called "Gallery"The difference isn’t huge, the steps are largely the same, and there’s an argument to be made for writing in a more declarative style, but there’s one crucial difference: the first feature is code, the second isn’t.
The argument against cucumber that’s often presented is that as a programmer, plain text is unnecessary, because we can all read code. While it’s true that we all can read code, I still find it beneficial to jump out of the code writing mode for describing the behaviour of the application. When you’re writing features first, you don’t want to be bothered with the details of how this functionality works. In this initial stage you care nothing about the implementation, about how the result is achieved. You care nothing about things like #document_form_container or .ui-dialog-buttonpane.
I believe that it’s in this switching between designer mode and developer mode where cucumber, done right, really shines.
In order to evaluate the bigger picture before hacking As a developer I want to write my stories before writing my codeThere are some secondary benefits as well. Writing truly plain text features leads to better maintainability as well, since the features are robust against code changes. Plain text is also easier to understand for new developers coming to an existing project. Probably the nicest advantage though is that over time a library of steps is built up, which can then be simply combined to describe new features.
The above feature is nicely illustrative of this anti-pattern, but it is far from the only example. In many of our cucumber suites here at Elabs, we have steps like the above, some of them were written by me. Which leads me to what’s really wrong with the last three lines of the above feature. They are written using nothing else than the standard web steps generated by cucumber-rails own generator. Cucumber itself ships with steps which in my opinion encourage an anti-pattern.
Pickle my fancy
Another tool which we’ve experimented a bit with is Pickle, which allows you to easily generate models from your feature files. A basic example from the README:
Given a user exists And a post exists with author: the user Given a person: "fred" exists And a person: "ethel" exists And a fatherhood exists with parent: user "fred", child: user "ethel"It actually looks fairly nice, reads quite naturally, so a first instinct might be to call this plain text. But on closer inspection, there is a whole language in there. To comprehend what these steps are doing you’d need to understand not only the domain models involved, but also the language Pickle uses to manipulate these. I’m pretty sure a non-technical person couldn’t make sense of the above. This is really no different, and in fact worse, than writing actual code:
@user = User.make Post.make(:user => @user) @fred = Person.make(:name => 'Fred') @ethel = Person.make(:name => 'Ethel') Fatherhood.make(:user => @fred, :child => @ethel)Note how there is an almost one-to-one mapping between the feature above, and the code below. The only thing cucumber does in this case is act as some kind of phoney translator. We write code, but not actual code. So we can do some stuff, but mostly it comes out worse than if we’d just written it as code in the first place. I can’t blame anyone for disliking cucumber when using it like this.
However, try this instead:
Given there is a user called "Jimmy" And there is a post authored by "Jimmy" Given there is a person called "Fred" And there is a person called "Ethel" And "Fred" is the father of "Ethel"There’s not a huge difference between the first couple of lines, even though they read somewhat nicer when written out like this. The real difference is in the last line. Here cucumber is adding value by explaining this abstract concept of a Fatherhood into something very concrete: one person is the other’s dad. Cucumber added value to this feature, instead of only acting as a hindrance.
I believe that Pickle is flawed as a concept, in order to achieve readable steps, they need to be written by hand.
The worst feature ever written.
As a curiosity, I present the worst cucumber feature known to man. If you are responsible for something like this, please go slap yourself in the face as hard as you can.
Scenario: User creates some sites and circuits, check connected sites list Given a "site" exists with {"name"=>"Somewhere1", "identifier" => "TER1", "provider"=>"TER1 Provider"} And a "site" exists with {"name"=>"Somewhere2", "identifier" => "TER2", "provider"=>"Some Provider"} And a "site" exists with {"name"=>"Somewhere3", "identifier" => "TER3", "provider"=>"TER3 Provider"} And a "circuit" exists with {"provider_name"=>"Another provider", "redacted_circuit_id"=>"ABC1", "provider_circuit_id"=>"C1", "circuit_type"=>CircuitType.find_by_name("Peering"), "service_type"=>CircuitServiceType.find_by_name("Dark Fiber"), :capacity => CircuitCapacity.find_by_name("1 Gbps"), "physical_wire_type"=>PhysicalWireType.find_by_name("Multi Mode Fiber"), "status"=> CircuitStatus.find_by_name("Cancelled"), "a_end"=>Site.find_by_identifier("TER1"), "b_end"=>Site.find_by_identifier("TER2")} And a "circuit" exists with {"provider_name"=>"Switch and Data", "redacted_circuit_id"=>"ABC2", "provider_circuit_id"=>"C2", "circuit_type"=>CircuitType.find_by_name("Backbone"), "service_type"=>CircuitServiceType.find_by_name("Dark Fiber"), :capacity => CircuitCapacity.find_by_name("1 Gbps"), "physical_wire_type"=>PhysicalWireType.find_by_name("Multi Mode Fiber"), "status"=> CircuitStatus.find_by_name("Cancelled"), "a_end"=>Site.find_by_identifier("TER1"), "b_end"=>Site.find_by_identifier("TER3")} When I am on the "connected_sites" page for site "TER1" Then the "connected-sites-list" should look like | Site ID | Site Name | Site Provider | Provider Circuit ID | Provider Name | Circuit Status | | TER2 | Somewhere2 | Some Provider | C1 | Another provider | Cancelled | | TER3 | Somewhere3 | TER3 Provider | C2 | Switch and Data | Cancelled | When I am on the "connected_sites" page for site "TER2" Then the "connected-sites-list" should look like | Site ID | Site Name | Site Provider | Provider Circuit ID | Provider Name | Circuit Status | | TER1 | Somewhere1 | TER1 Provider | C1 | Another provider | Cancelled | When I am on the "connected_sites" page for site "TER3" Then the "connected-sites-list" should look like | Site ID | Site Name | Site Provider | Provider Circuit ID | Provider Name | Circuit Status | | TER1 | Somewhere1 | TER1 Provider | C2 | Switch and Data | Cancelled |Yes, those are Hashes inside a feature, which are then eval’d. Make sure to scroll to the right to experience the full horror of it all. I challenge anyone to find a worse cucumber feature than this. I assure you, that thing is real (from one of our rescue mission projects), and there is much more where it came from.
Writing better steps
So how do we write better steps? For me personally, I’ve found that sticking to the following rule seems to lead to nice, maintainable steps:
A step description should never contain regexen, CSS or XPath selectors, any kind of code or data structure. It should be easily understood just by reading the description.
Some more advanced techniques.
In a previous post, I gave you some introductory information on Cucumber, a great framework for writing and executing high level descriptions of your software’s functionality. In this post, I’ll take a deeper dive and talk about a few more advanced Cucumber topics: project structures, multiple language support, scenario tables, free-form stories, tags, hooks and backgrounds. As always, for more detailed information see the documentation and/or The RSpec Book.
Project Structure
Let’s start by taking a look at your project structure: the usual advice is to have a features directory as the root of your Cucumber work. In that directory, you place all of your .feature files which contain your features (as you would expect) as well as support and step_definitions directories.
The support directory should contain whatever support code your features need, and an env.rb file which is responsible for loading any required code that lives outside the feature directory tree.
In the step_definitions directory, you place the files (with .rb extensions) that contain your step definitions. These will all get loaded when your features run.
You can have multiple subdirectories in the features directory for grouping features. This allows you to run the features in a particular directory. While this can be useful, it can be awkward in practice. That’s because (as of this writing) cucumber loads each ruby file (ending in .rb) it finds in the directory you tell it to run and, recursively, all subdirectories. For example, consider the following tree (each .rb files simply has a puts "x" where x is the name of the file):
features +- 1.rb +- 2.rb +- sub1 +- 3.rb +- sub2 +- 4.rb +- sub3 +- 5.rb +- sub4 +- 6.rbSo when you run cucumber features, you get:
1 2 3 4 5 6 0 scenarios 0 steps 0m0.000sbut, if you run cucumber features/sub2 you get:
4 5 6 0 scenarios 0 steps 0m0.000sThe issue is that features don’t inherit support code or steps from parent directories unless that parent is also visited by Cucumber. So if you want to run subdirectories separately and they share setup code or steps, you have to somehow duplicate that code (possibly by explicitly requiring files from up the tree). This isn’t significant, but it is a bit messy. A better approach might be to use tags (described below). If your features groups don’t share much, then using subdirectories can work fine.
Languages
In the referenced previous post I talked briefly about Cucumber’s multiple language support; here’s I’ll show you how it’s done, and how you can add your own if required.
The file cucumber/lib/cucumber/languages.yml defines the ‘natural language’ support of Cucumber. This is a yaml file that provides multi-lingual aliases for Gherkin keywords. As an example, here’s the entries for English, LOLZ, and Japanese:
"en": name: English native: English encoding: UTF-8 feature: Feature background: Background scenario: Scenario scenario_outline: Scenario Outline examples: Examples|Scenarios given: Given when: When then: Then and: And but: But space_after_keyword: true "en-lol": name: LOLCAT native: LOLCAT encoding: UTF-8 feature: OH HAI background: B4 scenario: MISHUN scenario_outline: MISHUN SRSLY examples: EXAMPLZ given: I CAN HAZ when: WEN then: DEN and: AN but: BUT space_after_keyword: true "ja": name: Japanese native: 日本語 encoding: UTF-8 feature: フィーチャ|機能 background: 背景 scenario: シナリオ scenario_outline: シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ examples: 例|サンプル given: 前提 when: もし then: ならば and: かつ but: しかし|但し space_after_keyword: falseTo see what languages are available in the version of Cucumber you have, issue the following command:
cucumber --language helpIf your desired language isn’t supported, clone aslakhellesoy/cucumber and add it. If you do, please send aslakhellesoy a pull request to have it considered for inclusion.
To run features written in a specific language, use the --language flag followed by the 2 (or so) letter abbreviation of the desired language. For example:
cucumber --language jaWhenever I’m talking about Gherkin keywords, I’ll use the English form. Everything said applies to any language version.
Free-form Story/Text
As mentioned in the intro post, following the feature header, you can have any amount of free-form text. This is generally used to give further background or explanation of the feature. Here’s an example from The RSpec Book:
Feature: code-breaker submits guess The code-breaker submits a guess of four colored pegs. The mastermind game marks the guess with black and white "marker" pegs. For each peg in the guess that matches color and position of a peg in the secret code, the mark includes one black peg. For each additional peg in the guess that matches the color but not the position of a color in the secret code, a white peg is added to the mark. Scenario Outline: submit guess ...Scenario Outlines and Example Tables
Sometimes you will have a collection of scenarios that are structurally all the same, differing only in some set of values. It might be nice to represent these as a table. In fact, the FIT project is aimed at doing just this. However, it can be nice to use a single tool to solve a given class of problem, and so Cucumber supports this style of testing.
There are two pieces to this capability. First is the definition of the scenario outline. This is a skeleton of the scenario, written with placeholders for the actual values. Continuing on from the previous feature:
Scenario Outline: submit guess Given the secret code is <code> When I guess <guess> Then the mark should be <mark>This is very similar to a regular scenario definition, with two exceptions. First, you use Scenario Outline: instead of Scenario:. This is what informs the system that you want to do a tabular style scenario. The second difference is the use of placeholders, e.g. <guess>.
The second piece is a data table (or tables). These start with the Scenarios: or Examples: keyword. Following the keyword is a textual description of the data in the table. Then we have a table header that serves to map the columns to the placeholders in the scenario. Following that is the data for each case the scenario should be applied to, one per line. Table cells are bracketed by vertical bars (i.e |):
Scenarios: all colors correct | code | guess | mark | | r g y c | r g y c | bbbb | | r g y c | r g c y | bbww | | r g y c | y r g c | bwww | | r g y c | c r g y | wwww | Scenarios: 3 colors correct | code | guess | mark | | r g y c | w g y c | bbb | | r g y c | w r y c | bbw | | r g y c | w r g c | bww | | r g y c | w r g y | www | Scenarios: 2 colors correct | code | guess | mark | | r g y c | w g w c | bb | | r g y c | w r w c | bw | | r g y c | g w c w | ww | Scenarios: 1 color correct | code | guess | mark | | r g y c | r w w w | b | | r g y c | w w r w | w |Any number of scenario tables can be used, and all apply to the most recent scenario outline. This results in far less copy/paste and the associated potential for error, as well as being a far more concise format.
Tags
In the simplest case, Cucumber runs all the scenarios in all the features that you point it at. By using tags you can be more specific about what is run. In my opinion, this is a killer feature. You tag features or scenarios by prefixing them with one or more tags, separated by spaces. A tag is simply an identifier prefixed by @. For example:
@billing @annoy Feature: Verify billing @important Scenario: Missing product description Scenario: Several productsTo run features and/or scenarios with specific tags you use the --tags command line flag and provide a comma separated list of tags. Specifying a feature with a tag runs all scenarios in the feature. Here’s a couple of examples based on the above code:
cucumber --tags @billing # Runs both scenarios cucumber --tags @important # Runs the first scenarioYou can also specify tags not to run, like so:
cucumber --tags ~@important # Runs the second scenario (Scenarios without @important) cucumber --tags ~@important,~@other # Won't run tasks tagged @important or @other.Hooks
Cucumber provides the ability to supply hooks to modify the behavior of executing features. Hooks can be used at the various levels of granularity, generally before and after, and are often defined in your env.rb file. Hooks are executed whenever the event they are defined for occurs.
Global Hooks
Global hooks run when Cucumber begins and exits. Begin hooks are informal: simply put the desired code in a file in the features/support directory (possibly env.rb, but I’d advise keeping it separate).
An exit hook is more formal, using at_exit. Here’s an example of a pair of global hooks:
#the begin 'hook' my_heavy_object = HeavyObject.new my_heavy_object.do_it at_exit do my_heavy_object.undo_it endScenario Hooks
You can add blocks that will run before and after each scenario:
Before do # Do something before each scenario. end After do |scenario| # Do something after each scenario. endThe After block will be passed the scenario that just ran. You can use this to inspect its result status by using the failed?, passed? and exception methods. For example:
After do |scenario| if(scenario.failed?) subject = "[Project X] #{scenario.exception.message}" send_failure_email(subject) end endStep Hooks
Cucumber gives you the ability to define a block that will be executed after each (and every) step:
AfterStep |scenario| do # Do something after each step. endTagged Hooks
If you’ve tagged some of your scenarios, you can also tag scenario and step hooks. You simply pass the tags as arguments to the hook methods). These tagged hooks will only be executed before/after scenarios that are tagged the same and after steps in scenarios tagged the same. Here’s an example where the hooks will only be run before scenarios tagged with @cucumis or @sativus.
Before('@cucumis', '@sativus') do # Do something before scenarios tagged @cucmis or @sativus end AfterStep('@cucumis', '@sativus') do # Do something after steps tagged @cucmis or @sativus endBackground
A Background is very much like a scenario in that it consists of a series of steps. The difference is that its steps are executed before the steps of each scenario in the feature. It’s basically a factoring out of a set of common lead-in steps for the features scenarios. One thing to remember is that a Background is run after any Before hooks.
You’ll generally only have given Backgrounds. Here’s an example:
Feature: User Login Background: Given account 'A123' for 'Dave' with password '123' And account 'B456' for 'Joe' with password 'abc' Scenario: Dave logs in and sees his account When I log in as 'Dave' using password '123' Then I am in account 'A123' Scenario: Jow logs in and sees his account When I log in as 'Joe' using password 'abc' Then I am in account 'B456'Summary
So that’s a quick look at some of the more advanced features of Cucumber. It’s a great tool, with a growing community behind and around it. I’ll be posting on Cucumber Best Practices next time, so keep an eye out. Enjoy, and comment with any questions!