Rails Testing Handbook
Rails Testing Handbook
Handbook
BY SEMAPHORE
1
Table of contents
01 Introduction 4
02 Behavior-driven development 6
07 Final Words 61
2
Rails Testing Handbook
BDD is one of the things that Semaphore developers practice every day.
Rails Testing Handbook will show you how to do it right.
If you’d like to read more, we regularly share our thoughts on engineering,
product development, and testing on our Semaphore blog.
This guide was created by Marko Anastasov, Stefan Mijučić, Igor Šarčević,
Milica Maksimović, and Dunja Radulov.
Cover and chapter illustrations: Tamara Čubrilo
© Copyright warning
Please don’t share this book, use any content or imagery, or otherwise try to use it for your own gain. If
you do share or write about it somewhere please give Rendered Text appropriate credit and a link.
3
CHAPTER 1
Introduction
Back in 2008, I was working on a Rails app where one of its features was a
multi-step reservation process. Each step had many possible states, some of which
were simply static options, and some were branching into a different workflow.
We would sometimes write unit tests, but only after finalizing work on a complex
method or a class.
Getting anything done was hard. We’d change one thing, then spend about an hour
on manual testing in the web browser to check if everything is still working. We’d
often be surprised by the fact that we broke a seemingly unrelated use case.
Sometimes we’d figure that out on our own, and sometimes we’d ship a new version
to production and the client would email us about what had just stopped working.
Getting that kind of news felt the worst. It felt like we were wasting a lot of our
client’s and our own time. We knew we could do better than that.
Then, we discovered Cucumber. There was a part on its website saying that it was
built for “behavior-driven development (BDD)” and “acceptance testing”, but all we
saw was that we could define a test once, and Cucumber would automatically
launch a web browser, run the application, and do the work of feature verification
instead of us.
4
That felt like magic. As soon as we started writing Cucumber scenarios for the
feature set we’ve been working on, our development accelerated by an order of
magnitude. We could add, remove and change code without worrying that we would
break something. When something did break, a failing scenario would let us know,
and we’d quickly fix it before deploying a new version to users.
From that point on, we wanted to write runnable test scenarios for every feature that
we’d work on. Then, we wanted to have them before we even wrote a single line
of implementation code. Afterwards, we wanted tests for every layer of application
code below, too. We continued happily working on that project for years to come.
Perhaps you, dear reader, are in a similar situation. Maybe you’ve heard that there’s a
connection between writing tests first and good design, and you want to become a
better programmer. Or maybe you’re familiar with test-driven development in
another language, but want to develop the habit specifically for Rails.
This book aims to provide that upgrade. Our discussion and examples will be
practical, because our knowledge comes from practicing BDD in developing real
products — not just theorizing about it. Over the past decade, our company
Rendered Text has evolved from being a small Rails consultancy to helping thou-
sands of organizations deliver better code faster with Semaphore, our CI/CD product.
All that time we’ve been doing BDD, and it has helped us write robust, sustainable
code at a steady pace — regardless of whether the app has 2,000 lines of code or
50,000.
Marko Anastasov
Rendered Text & SemaphoreCI.com cofounder
5
CHAPTER 2
Behavior-driven Development
Introduction
Behavior-driven development (BDD) is about minimizing the feedback loop between
business owners and developers. It is a logical step forward in the evolution of
software development. In this section, we’ll explain the concepts behind BDD and its
origin.
6
Waterfall
If you are a software developer or an engineering manager, you are probably familiar
with the Waterfall model, recognizable by the following diagram:
Waterfall model
SYSTEM REQUIREMENTS
SOFTWARE REQUIREMENTS
ANALYSIS
PROGRAM DESIGN
CODING
TESTING
OPERATIONS
What was later named “Waterfall” was first formally described by Winston Royce in
his 1970 paper “Managing the development of large software systems”. Most people
assume that this process was presented as the ultimate solution at the time.
However, Royce recognized that having a testing phase at the end of the develop-
ment process is a major problem.
This model is still used to develop software in many companies across the world.
Waterfall implies flow, but in practice there are always feedback loops between
phases. All major improvements to the model over time have been made by minimiz-
ing the feedback loops and making them as predictable as possible. For example, if
we write a program, we want to know how long it will take us to find out if it works.
On the other hand, if we design a part of the system, we want to find out if it is actu-
ally programmable and verifiable, and at what cost.
So, when we look at a feedback loop, we look for methods we can use to minimize
it. At first, our goal is to remove obviously wasteful work. Later, we start realizing
that we are able to optimize and do things faster and better than we could have ever
imagined back when we were doing things the old way.
7
The First Optimization: Test-first Programming
The first optimization happened by addressing the Coding and Testing phases.
In the traditional quality assurance-based (QA-based) development model, a pro-
grammer writes code and submits it in some way to the QA team. It takes a day, a
few days, or weeks to get a report if the code, and the rest of the program work. A
lot of times there are bugs, so we need to go back to programming and fix all the
issues.
CODING TESTING
“Bugs”
To cut down the feedback loop, we start coding and verifying at the same time. First,
we write some code, and then we write some tests for it. Tests produce an excellent
side effect, the automated test suite, which we can run at any time to verify every
part of the system for which we have written a test. Afterwards, we want to have a
test suite that covers the entire system, so that we can work as safely as possible.
The feedback loop of coding followed by testing still takes some time, so we invert
it, and we start writing tests before writing a single line of code. The feedback loop
becomes very small, and we soon realize that we are writing only the code we need
in order to pass the tests that we wrote.
CODING
TESTING
This is called test-first programming. When we work test-first, we use the tests
we write to help us “fill in” the implementation correctly. This reduces the number of
bugs, increases programmer productivity, and positively affects the velocity of the
whole team.
8
Test-driven Development
Once we have a continuous loop of testing and coding, we’re still doing all our pro-
gram design upfront. We’re using test-first programming to make sure that our code
works, but there’s a feedback loop where we may find out (disturbingly late) that a
design is difficult to test, impossible to code, performs badly or just doesn’t fit
together with the rest of the system.
CODING
DESIGN
TESTING
To minimize this loop, we apply the same technique. We invert it by doing test-first
programming before we start designing. In other words, we do the Testing, Coding
and Program Design all at the same time. A test influences the code, which in turn
influences the design, which influences our next test.
TESTING
CODING
DESIGN
Test-driven development
This is test-driven development (TDD). It drives our design ideas in an organic way,
and we implement only the parts of the design that we need, in a way which can
easily evolve too. Design now includes a substantial refactoring step, which gives us
confidence to under-design instead of over-engineer. We end up having just enough
design and appropriate code which meets our current requirements.
9
TDD combines test-first programming with design thinking by continuous
application of refactoring principles and patterns. The positive side effects are now
amplified. We have reduced the number of bugs, and we are not writing any code
that doesn’t help us with implementation. This further increases a team’s productiv-
ity by helping it avoid design mistakes which are more costly to fix the later they are
detected.
TESTING
ANALYSIS CODING
DESIGN
We can apply the same technique and bring Analysis in our loop. Now, we test-drive
a feature before we try to implement another. The duration of such cycles for a
developer is measured in hours, sometimes even minutes, not days or weeks.
TESTING
CODING ANALYSIS
DESIGN
10
After applying this technique consistently for a while, we notice that we tend to
break down all features in the smallest units and consistently deliver them one by
one. We improve our understanding of how features affect one another and find
ourselves being able to respond to changes faster. We can identify and discard un-
wanted features quickly and give priority to important features.
Behavior-driven development
TESTING CODING
ANALYSIS DESIGN
This is called behavior-driven development. It saves time for both the stakeholders
(business owners) and the development team. By asking questions early, developers
help both themselves and the stakeholders gain a deep understanding of what it is
that they are building. Stakeholders get results at a predictable pace and, since the
features are worked on in small chunks, estimates can be done more accurately, and
new features can be planned and prioritized accordingly.
Lean Startup would be the closest concept that brings together gathering require-
ments, feature development and marketing as a way to close the loop on learning
what a startup needs to build. Of course, the process goes somewhat differently in
enterprises, although they are learning to apply the lean startup principles in many
projects as well.
11
Continuous Delivery
Merging BDD with deployment and operations brings us to the broad concept of
continuous delivery. The most important processes are continuous integration (CI)
and continuous deployment, which you can easily configure for any project on
Semaphore.
Summing up
Behavior-driven development evolved from optimizing various phases in the
software development process. By analyzing, testing, coding and designing our
system in one short feedback loop, we are able to produce better software by
avoiding mistakes and wasteful work.
It is a common misconception that TDD is about testing and that, since it has its
origins in TDD, BDD is just another way of approaching software testing. This is not
the case, although tests are a nice byproduct. TDD is a holistic approach to software
development, derived from one simple idea: wanting to optimize the feedback loops
in our work.
None of the steps required to practice BDD are required to make software in general.
They also take time to learn and apply effectively. Still, their payoff of a sustainable
process that enables us to continuously produce working software is well worth the
investment.
12
CHAPTER 3
Applying BDD to
Ruby on Rails Web Applications
Introduction
In this section, we’ll focus on applying BDD principles while developing a web
application using Ruby on Rails.
13
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. An example would be writing a test which asserts that
a collection of comments is specifically an array, and not one of its unique features,
such as being sorted by time. In most cases it shouldn’t matter if we change the
implementation of that collection to return a custom enumerable class. More general-
ly, changing the implementation of an object shouldn’t break its test suite as long as
what the object 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.
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.
14
Overview of BDD Tools for Rails
Ruby on Rails was the first web framework to ship with an integrated testing frame-
work. This 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 speci-
fied 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.
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.
15
Cucumber
As we explained in the first chapter of this guide, 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, the developer writes them. 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.
If this were a web application, the scenario above could automatically boot a test in-
stance of the application, open it in a web browser, perform steps as any user would
do, and then check if certain expectations have been met.
1. 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.
2. 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).
16
3. Write a definition of the first failing or pending spec. Run the scenario and
watch it fail.
5. 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 tak-
en care of, you will know what the models or your custom objects should do.
6. 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.
7. Once you have implemented all the objects and methods you need and the corre-
sponding specs are passing, run the Cucumber scenario you started with to make
sure that the step is satisfied.
WRITE A SCENARIO
FAIL PASS
REFLECT ON REFACTORING
IMPLEMENT A STEP
AND VALIDATE WITH
DEFINITION
STAKEHOLDERS
WRITE A
FAILING SPEC
WRITE CODE TO
MAKE SPEC PASS
In order:
1. View
2. Controller REFRACTOR
3. Model / domain object
STEP PASSES
17
Once the first scenario step passes, move on to 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 section, we explored how to apply behavior-driven development when devel-
oping a web application using Ruby on Rails, step by step. At this point you should
be ready to start writing code the BDD way.
18
CHAPTER 4
Introduction
In this chapter we will guide you through the process of generating a new Rails 5
application, with all the necessary tools to set up a behaviour-driven development
(BDD) flow.
When you’re finished setting up the project on your machine, we will set it up on
Semaphore and establish a fully automated continuous integration workflow.
19
System Prerequisites
To follow our guide, you need to have the following installed on your Unix-based
machine.
• Git,
• Ruby 2.4.0,
• Node.js, and
• PostgreSQL 9.5.
The -d postgresql specifies that our application will use PostgreSQL as our database
management system instead of SQLite, which is set as a default for Rails.
cd bdd-app
By passing the --path parameter we are telling bundler to install gems in the
bdd-app/vendor/bundle directory. If you leave off the parameter, gems will be in-
stalled globally, which isn’t a good practice if you are working on more than one Ruby
application on the development machine.
20
Installing RSpec
First, we will set up RSpec for writing unit specs.
Start by adding rspec in the Gemfile file, under the development and test group:
To finish the install, invoke the Rails generator to set up the spec directory in your
application.
When the command finishes, you should have a new spec directory in your project.
This directory is the base for all of your unit specs.
Installing Cucumber
Next, we will continue to set up Cucumber, the tool used for writing acceptance
tests.
To finish the install, invoke the rails generator to set up the features directory in
your application:
21
$ bundle exec rails generate cucumber:install
When the command finishes, you should have a new features directory in your
project. This directory is the base for all of your acceptance tests.
Installing Shoulda-matchers
Shoulda-matchers gem speeds up our testing time by using wrappers around
common Rails functionality, such as validations, associations and redirects.
bundle install
We need to configure this gem by specifying the test frameworks and libraries we
want to use it with. Open spec/rails_helper.rb and paste the following block at the
end:
22
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :active_record
with.library :active_model
with.library :action_controller
with.library :rails
end
end
Installing FactoryBot
Factory Bot is a gem used for making “fake” objects as test data. It is essentially a
fixtures replacement with a clear definition syntax. It allows you to create fake ob-
jects in tests without providing a value for each attribute of the given object. If you
don’t provide any values to the attributes of the object, factory_bot uses the default
values that are previously defined in factory’s definition.
Add the following line to the development and test group of your Gemfile:
bundle install
After the installation, you can place factories for your database models in the
spec/factories directory.
We want to make sure that the state of the application is consistent every time we
run our tests. The best way to do this is to clean all the data in the database between
each test run, and construct a new clean state for each test.
The database-cleaner gem is a set of strategies for cleaning our database between
test runs.
23
First, add the gem to the :test group in your Gemfile:
# Gemfile
group :test do
gem ‘database_cleaner’
end
When the gem is installed, we will set up a cleaning strategy for RSpec tests. Open
the spec/rails_helper.rb file, and add the following snippet:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
gem ‘rails-controller-testing’
RSpec automatically integrates with this gem. Adding the gem to your Gemfile is
sufficient for the helpers to work in your controller specs.
24
Setting up a Repository
We will use GitHub to set up your repository for this project.
First, let’s make sure that our repository is ready to be published and that we won’t
push any unnecessary file or directory to GitHub.
Add the vendor/bundle directory to your .gitignore file to prevent pushing gems to
Github.
git add .
git commit -m “Bootstrapping Rails BDD application”
Visit https://github.com/new and create a new repository for your project. GitHub will
give you the instructions how to connect your local repository with the remote one.
25
Finally, add the new remote and push your code to GitHub.
The first step is to create a free Semaphore account and then add a new project to
Semaphore.
Follow the setup guide. Semaphore will recognize that you have a Rails project with
Postgresql, and will generate all the commands for a successful first build.
Let’s tweak the command list, and add our RSpec and Cucumber test suites.
26
Hit Build With These Settings to start the first build for your project.
27
Conclusion
Congratulations! You’ve set up a great foundation for behaviour-driven development
with Rails 5, RSpec and Cucumber.
28
CHAPTER 5
Introduction
Most of web facing applications need some sort of an authentication mechanism to con-
trol access to the internal resources of the system. This is a natural continuation to the
previous chapter where we’ve set up a base for behaviour driven development.
In this chapter, we will cover setting up a user authentication system in Rails using the
Devise gem.
29
Defining our Goals
Before we start writing any code, we should define our end goals. This will focus our
attention and help us write code more efficiently.
To cover all the basic use cases, we want to allow users to sign up, log in, and log
out. Let’s write down these requirements in a Cucumber feature file.
# features/authentication.feature
Feature: Authentication
A Cucumber feature starts with a title and a comment written in plain English. While
the original goal of this syntax design was to allow non-technical people to under-
stand the scenarios, this also helps developers clarify their thoughts. By writing a
scenario in advance, we define the scope of the scenario and spell out the functional-
ity in plain English, effectively codifying the application design. This helps clarify our
thoughts and manage the work that’s ahead of us.
We continue with defining scenarios that describe the behaviour of our feature. First,
the sign up scenario:
# features/authentication.feature
Scenario: Signing up
Given I visit the homepage
When I fill in the sign up form
And I confirm the email
Then should see that my account is confirmed
# features/authentication.feature
30
# features/authentication.feature
Cucumber helps us out at this point. If we run bundle exec cucumber, it displays a list
of steps that are not defined.
Let’s use the output of Cucumber to create a step definition file for user
authentication:
# features/step_definitions/authentication_steps.rb
31
We have now our Cucumber steps, however all of them are still pending. Let’s define
them one by one.
A step definition is nothing more than plain Ruby code. You can write anything in
the step definitions, but usually we use Capybara helpers to set up tests, and RSpec
expectation to verify that the system ended up in the desired state.
We will use Capybara to navigate our application. It defines helpers for visiting pages
in our application with the visit helper, clicks on links with click_link helper, and it has
more features that are described in its documentation.
For the email verification steps, we will use the email_spec gem that provides
helpers such as open_email, visit_in_email, and other email handling shortcuts that
are very useful for testing.
visit_in_email(“Confirm my account”)
end
The Then steps are usually used to set up expectation on the system. In our case,
we will use the expect assertion to test whether the current page has the Your email
address has been successfully confirmed message.
expect(page).to have_content(message)
end
32
In the “I am a registered user” step we will make sure that there is an existing user
account in our system. For this purpose, we will assume that we have a FactoryBot
factory that can create a valid user account in our system.
The rest of the steps are a variation of the above cases. We either use Capybara to
navigate our application and fill in forms, or we use the RSpec expect matcher to
verify that a message appeared on the screen.
Every step is now defined. Let’s run Cucumber to see what is the current state of our
authentication system:
33
$ bundle exec cucumber
Failing Scenarios:
cucumber features/authentication.feature:7 # Scenario: Signing up
cucumber features/authentication.feature:13 # Scenario: User Logs In
cucumber features/authentication.feature:19 # Scenario: User Logs Out
3 scenarios (3 failed)
13 steps (3 failed, 10 skipped)
0m0.193s
All our tests are failing. This is the expected outcome as we have not yet
implemented any code to support it.
Setting Up Devise
Devise is the de-facto authentication system for Rails applications. It takes care of
storing user passwords securely and it comes prepackaged with all the views neces-
sary for the user account management.
gem ‘devise’
Following a successful installation, run this command to generate Devise views, con-
trollers, and models in your application:
The output of the devise:install command will suggest several changes in the appli-
cation. We will use several of them.
To make the user mailer work in our local development environment, we will add the
following snippet to the config/environments/development.rb and config/environ-
ments/test.rb files:
34
Add a route to our homepage so that Devise knows where to redirect registered
users:
Devise is now installed. Let’s run Cucumber again to see what is our progress:
Scenario: Signing up
Given I am on the homepage
uninitialized constant HomeController (ActionController::RoutingEr-
ror)
./features/step_definitions/authentication_steps.rb:2:
in `”I am on the homepage”’
features/authentication.feature:8:in `Given I am on the homepage’
And I fill in the sign up form
And I confirm the email
Then I should see that my account is confirmed
...
35
First, create an empty Home controller:
def index
end
end
request.formats: [“text/html”]
request.variant: []
Based on the Cucumber error, we can deduce that we need to create a view for our
controller action.
# spec/controllers/home_controller_spec.rb
require “rails_helper”
RSpec.describe HomeController do
let(:user) { instance_double(User) }
before { log_in(user) }
expect(response.status).to be(200)
end
end
end
In the previous unit test, we have made use of the log_in helper to simulate a user
who is logged in in our controller unit specs. We need to define this helper in our
spec/rails_helper.rb test helper file.
First, load all the Devise test helpers, and a custom ControllerHelpers class.
36
require “support/controller_helpers”
RSpec.configure do |config|
config.include Warden::Test::Helpers
config.include Devise::TestHelpers, :type => :controller
config.include ControllerHelpers, :type => :controller
module ControllerHelpers
def log_in(user)
warden = request.env[‘warden’]
allow(warden).to receive(:authenticate!).and_return(user)
allow(controller).to receive(:current_user).and_return(user)
end
end
With this, the log_in helper is available in all our controller specs.
$ mkdir app/views/home
$ echo “<h1>Homepage</h1>” > app/views/home/index.html.erb
Running Cucumber again, we can now see that the first step is green. Our home
page is ready. Let’s protect it from non-authenticated users.
before_action :authenticate_user!
end
This will protect all our pages from non-authenticated users and redirect them to the
sign up page.
When we run Cucumber again, we can see that the second step in the sign up flow
is completed and green. The only thing that’s missing now is the email verification
step.
37
Verifying Email Verification Steps
First, add the email_spec gem to the Gemfile.
To include the email helpers in the Cucumber environment, add the following to the
features/support/env.rb file:
Running the Cucumber steps again, we can see that the email spec steps are working,
but the verification step is not sent.
[] (EmailSpec::CouldNotFindEmailError)
./features/step_definitions/authentication_steps.rb:15:
in `”I confirm the email”’
features/authentication.feature:10:in `And I confirm the email’
This is not a big surprise. By default, Devise does not send confirmation emails.
38
Add the following columns to the generated migration:
config.reconfirmable = false
# spec/factories/users.rb
FactoryBot.define do
factory :user do
confirmed_at Time.now
end
end
When you run Cucumber, you will notice that the scenario for signing up is green.
39
Let’s push our code to GitHub, and wait for Semaphore to turn green.
40
CHAPTER 6
Introduction
We’ve come a long way and covered the concept and theory behind behavior-driven
development, set up the tools, and mapped new concepts to a basic Rails application.
We are sure that you are eager to jump into the code and start following the BDD path
at this point.
However, pure theory is not enough for you to effectively bootstrap your BDD journey. A
sneak peek at a real world example will help you to close this gap.
We’re going to build a CRUD (short for create, read, update, delete) application that
helps the users keep track of their books. We will proceed in baby steps, writing our
application from outside-in.
Cucumber and RSpec will be our main tools. We will also cover the Git flow, give hints
how and when to communicate with your team, and how to make sure that a new fea-
ture is ready for shipping.
41
Understanding the Application
Our goal is to write a Rails-based web application that can keep an inventory of your
books. Users of our application will be able to add, edit and remove books from their
inventories.
While the scope of this project is small, we will write it in a way that will make it easy
to maintain it and further develop it. The project should be able to grow, and to be
safely and continuously updated. We will keep this in mind while writing specs and
implementation, making sure that our tests are not crude and that they can cope with
changing requirements.
Let’s switch to the master branch and make sure that we have the latest revision on
our local development machine:
Now, let’s create a new branch to make sure that our development doesn’t hinder the
productivity of our colleagues. We will call this branch book-inventory that reflects
the scope and our primary goal on this branch:
42
With Cucumber we can make sure that our end goals are solidified with a high level
language and reputedly tested.
A Cucumber feature starts with a title and a comment, written in plain English. While
the original goal of this syntax design was to allow non-technical people to under-
stand the scenarios, this also helps us, developers, clarify our thoughts. By writing a
scenario in advance, we define the scope of the scenario and write the functionality
in plain English, effectively codifying the application design. This helps clarify our
thoughts and manage the work that’s ahead of us.
# features/book_inventory.feature
Now, we start to peel the layers of the feature. We ask ourselves what basic as-
sumptions we have about the users who are using this feature. For a start, we want
to allow only registered and signed in users to keep track of their books:
# features/book_inventory.feature
Background:
Given I am a registered user
And I am logged in
We will now describe our first interaction with the system — listing books in the
inventory.
43
Scenario: Listing books in my inventory
Given I have populated my inventory with several books
When I visit the homepage
Then I should see the list of my books
You can implement step definitions for undefined steps with these
snippets:
Neat! Cucumber tells us that we have set up our goals, but we have not defined their
meaning.
The first step is to copy the output from Cucumber into a Cucumber step definition.
# features/step_definitions/book_inventory_steps.rb
Next step is to implement the step definitions and set up tests that verify the com-
pleteness of our new feature.
44
Don’t sweat if you don’t understand every part of the code bellow. Try to focus on
the flow and general principle of implementation.
# features/step_definitions/book_inventory_steps.rb
FactoryBot.create(:book,
:user => @registered_user,
:name => “Moby Dick”,
:author => “Herman Melville”)
end
In the above snippet, we use FactoryBot to inject records into the database.
The @registered_user user comes from the Given I am a registered user step defini-
tion that we defined in the previous chapter while setting up a user account manage-
ment system.
git add . && git commit -m “Describe book inventory listing” && git push
Creating a commit creates a safe point of return. If our implementation goes astray,
we can safely delete everything and return to this revision in Git and try again with a
different approach.
Apart from creating a safe point of return, we also want to validate that our specs are
set up correctly. We should have RED tests on our CI at this point.
45
Cucumber-driven Implementation of the Book
Inventory Listing
Cucumber tells us that the book model, along with its factory, is not available in our
system. This is not a surprise. We still need a lot of code to complete our feature,
from data models to views and factories.
Rails offers a quick and easy way to generate a good chunk of our implementation by
scaffolding.
46
We want to scaffold a book model that belongs to users. Every book will have a
name and an author. Let’s pass this information to Rails:
The first two steps are passing. We have satisfied a good part of our goals just by
generating a book model.
git add .
git commit -m “Generate book scaffold”
git push
47
Setting Up Unit Tests for the Book Inventory
Listing
The first two steps have passed, but we still need to deal with the third step. It
seems that our book is not listed on the page.
Before we continue, let’s set up specs for our controller and model so we can be sure
that individual units are working as expected. For unit testing we use RSpec.
Controllers are best tested with a mocking approach, since they tie many things to-
gether. The freedom you get from mocking should be used as an advantage in mold-
ing the controller method’s code to an ideal shape. This aids us to test the behavior
of our systems.
With this style of testing we focus on the interaction of our components instead of
focusing on the result of an operation. For example, we would write our tests for a
UserMailer as expect(UserMailer).to receive(:send_signup_email) instead of testing
whether the mail was successfully delivered. This is popularly called the London/
Interaction school of testing after London’s Extreme Tuesday Club where it became
popular.
# spec/controllers/books_controller_spec.rb
require “rails_helper”
RSpec.describe BooksController do
let(:user) { instance_double(User) }
before { log_in(user) }
before do
allow(user).to receive(:books).and_return(books)
get :index
end
end
48
In the above code block, we use the get method that is included in every controller
spec. Controller specs have helpers for other HTTP calls as well, covered in detail on
RSpec Rails documentation page.
RSpec tells us that users and books are not yet connected:
1) BooksController GET
#index looks up all books that belong to the current user
Failure/Error: allow(user).to receive(:books).and_return(books)
the User class does not implement the instance method: books
Running RSpec again validates our implementation. We now want to make sure that
our data layer and the associations are correct.
We will use shoulda-matchers to write specs for the user and book models.
# spec/models/book_spec.rb
require “rails_helper”
# associations
it { is_expected.to belong_to(:user) }
# columns
it { is_expected.to have_db_column(:name).of_type(:string) }
it { is_expected.to have_db_column(:author).of_type(:string) }
it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }
end
# spec/models/user_spec.rb
it { is_expected.to have_many(:books).dependent(:delete_all) }
end
The tests for the model are ready. Let’s connect our users and books in our
application:
# app/models/user.rb
49
Let’s write a minimal implementation of the index method that matches our
assumptions:
# ...
def index
@books = current_user.books
end
# ...
end
Running RSpec again tells us that every unit in our system passes our assumptions:
Now that we confirmed that every unit works, we can go back to Cucumber.
We still hit the same error. This is a good point to introduce debug steps in the Cu-
cumber steps:
# features/step_definitions/book_inventory_steps.rb
50
Run Cucumber again:
<!DOCTYPE html>
<html>
<body>
<h1>Hello</h1>
</body>
</html>
The root_path is not directed to the book index controller. Let’s edit Rails routes:
Rails.application.routes.draw do
resources :books
devise_for :users
At this point, the Home controller defined in the previous step becomes deprecated.
Delete it:
rm app/controllers/home_controller.rb
rm spec/controllers/home_controller_spec.rb
Now, we go back to delete the debug step and run Cucumber again:
1 scenario (1 passed)
5 steps (5 passed)
0m0.409s
51
A green test! Let’s run the whole test suite just to make sure that everything passes:
Failing Scenarios:
cucumber features/authentication.feature:13 # Scenario: User Logs In
Changing the root path broke our authentication step. Let’s rewrite the spec to follow
our new requirements:
# features/step_definitions/authentication_steps.rb
2 scenarios (2 passed)
9 steps (9 passed)
0m0.768s
git add .
git commit -m “Implement book inventory listing”
git push
We have now finished our first feature by following the BDD path.
52
Creating New Books
In the previous section, we have completed one full BDD cycle. Guided by high level
specs, we dug deep into unit tests, and finally to the implementation.
When you start with BDD for the first time, a lot of the steps seem cumbersome,
giving you the feeling that BDD drastically reduces productivity. However, this is very
far from the truth. As developers get used to BDD, we become more efficient with
defining Cucumber scenarios. Writing tests becomes second nature to most of us,
and the red -> green -> refactor cycle increases our productivity by making sure that
we can make changes in the code without breaking an existing feature.
We will continue to follow the BDD pattern to implement the rest of the book inven-
tory feature, but this time with an increased tempo.
Starting from the top, we define our goals for adding new books into the inventory.
# features/book_inventory.feature
# features/step_definitions/book_inventory_steps.rb
While defining step definitions, we heavily rely on Capybara to interact with the
browser. Commands like fill_in, click_link, and visit all come from here. There are
more commands available in the framework, ranging from simple UI interactions,
to more complex commands that allow you to run custom JavaScript on the page.
Keeping a browser tab open with Capybara’s documentation is an excellent way to
boost your Cucumber knowledge.
53
$ bundle exec cucumber
Failing Scenarios:
cucumber features/book_inventory.feature:16 # Scenario: Adding a new
book to the inventory
With the high level goals set in place, we dig deeper into individual units. This time,
we want to make sure that our controllers know how to create new books and attach
them to an existing user.
Let’s define a spec for the #create action, and make sure that a new book is created
and attached to the user.
# spec/controllers/books_controller_spec.rb
before do
allow(book).to receive(:save)
allow(user).to receive_message_chain(:books, :build).and_return(book)
end
expect(book).to have_received(:save)
end
end
54
The implementation should look as follows:
# app/controllers/books_controller.rb
def create
@book = current_user.books.build(book_params)
@book.save
end
Let’s now define what happens when the book creation succeeds. For this purpose,
we will introduce a new context that describes the state of the system after the save
action was called.
A good practice is to set up the context in a before step. In our case, the context is
that the save action returns true.
# app/controllers/books_controller.rb
def create
@book = current_user.books.build(book_params)
if @book.save
redirect_to @book, notice: ‘Book was successfully created.’
end
end
55
Let’s now cover the negative path with another context:
# app/controllers/books_controller.rb
def create
@book = current_user.books.build(book_params)
if @book.save
redirect_to @book, notice: ‘Book was successfully created.’
else
render :new
end
end
As we are always attaching the book to the current user, we can edit the book form
and remove the now deprecated user input field:
<div class=”field”>
<%= form.label :user_id %>
<%= form.text_field :user_id, id: :book_user_id %>
</div>
git add .
git commit -m “Add books to the inventory”
git push
56
Updating and Deleting
We will now finish the remaining two features — editing a book and removing a
book from the book inventory.
This time, we will implement two scenarios in one BDD cycle. As developers get
used to the tooling they can get more ambitious and implement more scenarios in
one go, sometimes a whole feature can be defined before the implementation.
As usual, let’s start from the top and define the Cucumber scenarios:
# features/book_inventory.feature
# features/step_definitions/book_inventory_steps.rb
click_link “Edit”
Then(/^I should see the book with the new title in my inventory$/) do
visit root_path
click_link “Destroy”
end
57
Now that the high level goals are set, it’s time to descend into the units. First, we’ll
set up tests for the #update action, covering the positive and negative paths of
execution:
# spec/controllers/books_controller_spec.rb
before do
allow(Book).to receive(:find).and_return(book)
allow(book).to receive(:update).and_return(true)
end
expect(book).to have_received(:update)
end
expect(response).to redirect_to(book_path(book))
end
end
expect(response).to render_template(:edit)
end
end
end
At each step, we make sure to run our test suite, and verify that the controller is im-
plemented correctly. In our case, Rails already generated a scaffold that matches our
specification.
Remember, even if Rails auto-generated code for your feature, it is crucial to cover
it with tests. They help us set up a fast feedback loop and make it easy to introduce
changes in the behaviour.
58
Next, let’s cover the #destroy method with unit specs.
# spec/controllers/books_controller_spec.rb
before do
allow(Book).to receive(:find).and_return(book)
allow(book).to receive(:destroy)
When all unit tests are green, we can zoom out and return to our Cucumber feature.
Everything should be passing at this point.
git add .
git commit -m “Updating and deleting books from the inventory”
git push
59
Finishing a Feature and Merging into Master
All our initial goals are implemented. Now, we prepare a pull request and ask our col-
leagues for a review. This often forgotten step is crucial for delivering good software.
We will use the name of the feature, book inventory, to name our pull request.
When Semaphore reports that our build is green, we can safely merge our feature
branch, and automatically deploy it without worrying that we’ll break something in
production.
60
Final Words
Congratulations, you’ve made it through the entire Rails Testing Handbook. We wrote this
book with the goal to help you write clean code and build maintainable
applications. Now it’s up to you — go make something awesome!
Further Reading
1. RSpec documentation
2. Cucumber documentation
3. Capybara documentation
4. Semaphore Engineering Blog
5. Ruby Tutorials on Semaphore Community
About Semaphore
Semaphore helps you continuously test and deploy Ruby code at the push of a
button. It lets you automatically parallelize your Ruby tests, get feedback right
inside pull requests, and deploy more often in a unified workflow. Already trust-
ed by thousands of businesses around the globe, Semaphore can help your team
move faster too.
You can use a coupon code RUBYBOOK1 to get a $50 credit on a new
Semaphore account.
61