Introduction
Behavior-driven development (BDD) is a software development practice of working in a short feedback loop, where we consistently apply test-driven development to every new feature we are exploring and working on. For a detailed introduction to the practice of BDD, you should read our article Behavior-driven Development. This article focuses on applying these principles in the practice of developing a web application using Ruby on Rails.
Understanding The “Behavior” Point of View
When applying test-driven development (TDD), developers can easily fall into the trap of using unit tests to test what an object or method is, rather than what it does, which is a lot more useful. A typical example would be writing a test that asserts that a collection of comments is specifically an Array, and not that it is, say, sorted by time. What one thing is internally is an implementation detail, and changing that implementation should not break a test as long as what it does remains the same.
BDD puts focus on behavior — what a thing does — on all levels of development. Initially, the word “behavior” may seem strange. Another way to frame this is to think about descriptions. We can describe every low-level method, object, button or screen to another person — and what we will be describing is exactly what a behavior is. Adopting this approach changes the way we approach writing code.
The “Given / When / Then” Communication Pattern
Most problems in software development are communication problems. For example, business owners fail to describe every use case of a proposed functionality, developers misunderstand the scope of a feature or a product team does not have a protocol to verify if a feature is done. BDD simplifies the language we use to describe scenarios in which software should be used: Given some context or state of the world, When something happens, Then we expect some outcome.
Given, When, Then are simple words we can use to describe a complex feature, code object or a single method equally well. It is a pattern that all members of the team in various roles can understand. These expressions are also built-in in many testing frameworks, such as Cucumber. A clear formulation of the problem and the solution (behavior) that we need to implement helps us write better code.
Overview of BDD Tools for Rails
Ruby on Rails was the first web framework to ship with an integrated testing framework. This lowered the barrier to entry to TDD and acted as a springboard for further advancements of the craft. At the same time, the expressiveness of Ruby and the productivity boost in developing web applications with Rails attracted many experienced and high-profile engineers to the community early on. These are the main reasons why most of the BDD tools and practices gained initial traction and why they have seen significant development in the Rails community.
When you generate a new Rails application with default options, it sets the scene for testing using test/unit, a testing library that comes with Ruby. However, there are tools which make BDD easier to apply. We recommend using RSpec as the main testing library and Cucumber for writing high-level acceptance tests.
RSpec
RSpec is a popular BDD testing library for Ruby. Tests written using RSpec — called specs — are executable examples of expected behavior of a piece of code in a specified context. This is much easier to understand by reading the following code:
describe ShoppingCart do
context "when first created" do
it "is empty" do
shopping_cart = ShoppingCart.new
expect(shopping_cart).to be_empty
end
end
end
Well-written specs are easy to read, and as a result, understand. Try reading the code snippet above out loud. We are describing a shopping cart, saying that, given a blank context, when we create a new shopping cart, we expect(shopping_cart).to be_empty
.
Running this spec produces output which resembles a specification:
ShoppingCart
when first created
is empty
We could use RSpec to specify an entire system, however we can also use a tool which helps us write and communicate using more appropriate (broad) terms.
Cucumber
As we have previously explained in the Introduction to BDD, we want to test-drive the analysis phase of every new feature. To do that, we need customer acceptance tests to drive the development of the code which will actually implement the feature. If you are a developer working in a sufficiently complex organization, you may want to have somebody else (a customer or product manager) write acceptance tests for you. In most cases however, it is the developer who writes them, and this is a good practice, because it helps us understand better what it is that we need to build. Cucumber provides the language and format to do that.
Cucumber reads plain text descriptions of application features, organized in scenarios. Each step in the scenario is implemented using concrete code, and it automates interaction with your application from the user’s standpoint. For example:
Feature: Reading articles
Scenario: Commenting on an article
Given I am signed in
And I am reading an article with "2" comments
When I reply to the last comment
Then the article should have "3" comments
And I should be subscribed to follow-up comments
If this was a web application, the scenario above could automatically boot a test instance of the application, open it a web browser, perform steps as any user would do, and then check if certain expectations have been met.
The BDD Cycle in Rails
In practice, BDD implies an outside-in approach: starting with an acceptance test, we begin writing code in the views and work our way down to the models. This approach helps us to discover any new objects or variables we may need to effectively implement our feature early on, and make the right design decisions based on this.
The BDD cycle in Rails consists of the following steps:
- Start with a new Cucumber scenario. Before you write it, make sure to analyze and understand the problem. At this point you need to know how the user interface allows a user to do a job. Do not worry about the implementation of scenario steps.
- Run the scenario and watch it fail. This will tell you which steps are failing, or pending implementation. At first, most of your steps will be pending (undefined).
- Write a definition of the first failing or pending spec. Run the scenario and watch it fail.
- Test-drive the implementation of a Rails view using the red-green-refactor cycle with RSpec. You’ll discover instance variables, controllers and controller actions that the view will need to do its job. This is also the only phase which has been proved to be optional in practice. An alternative approach is to simply prepare the views and controllers before moving on to the next step.
- Test-drive the controller using the red-green-refactor cycle with RSpec. Make sure that the instance variables are assigned and that the actions respond correctly. The controllers are typically driven with a mocking approach. With the controller taken care of, you will know what the models or your custom objects should do.
- Test-drive those objects using the same red-green-refactor cycle with RSpec. Make sure that they provide the methods needed by the controller and the view. If you are working on a new feature for which a model does not exist yet, you should now generate the model and the corresponding database migrations. At this point you’ll know exactly what you need them to do.
- Once you have implemented all the objects and methods you need and the corresponding specs are passing, run the Cucumber scenario you started with to make sure that the step is satisfied.
Once the first scenario step passes, move onto the next one and follow the same steps. Once your entire scenario has been implemented — the scenario is passing, along with all underlying specs — take a moment to reflect if there is something that you can refactor further.
Once you’re sure that you’ve completed the scenario, either move on to the next one, or show your work to others. If you work with a team, create a pull request or an equivalent request for a code review. When there are no more related scenarios left, show your work to your project manager or client, asking them to verify that you’ve built the right thing by deploying a feature branch to a staging server.
Moving On
In this article, we explored how to apply behavior-driven development when developing a web application using Ruby on Rails, step by step. At this point you should be ready to start writing code the BDD way. The following tutorials will help you with that:
- Setting Up a BDD Stack on a Rails 5 Application
- Setting Up a BDD Stack on a New Rails 4 Application
- Learn RSpec series
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.