The following is an excerpt from my book, The Complete Guide to Rails Testing.
What we’re going to do
One of the biggest challenges in getting started with testing is just that—the simple act of getting started. What I’d like is for you, dear reader, to get a small but meaningful “win” under your belt as early as possible. If you’ve never written a single test in a Rails application before, then by the end of this chapter, you will have written your first test.
Here’s what we’re going to do:
- Initialize a plain-as-possible Rails application
- Create exactly one static page that says “Hello, world!”
- Write a single test (using RSpec and Capybara) that verifies that our static page in fact says “Hello, world!”
The goal here is just to walk through the motions of writing a test. The test itself is rather pointless. I would probably never write a test in real life that just verifies the content of a static page. But the goal is not to write a realistic test but to provide you with a mental “Lego brick” that you can combine with other mental Lego bricks later. The ins and outs of writing tests get complicated quick enough that I think it’s valuable to start with an example that’s almost absurdly simple.
The tools we’ll be using
For this exercise we’ll be using RSpec, anecdotally the most popular of the Rails testing frameworks, and Capybara, a library that enables browser interaction (clicking buttons, filling out forms, etc.) using Ruby. You may find some of the RSpec or Capybara syntax confusing or indecipherable. You also may well not understand where the RSpec stops and the Capybara starts. For the purposes of this “hello world” exercise, that’s okay. Our goal right now is not deep understanding but to begin putting one foot in front of the other.
Initializing the application
Run the “rails new” command
First we’ll initialize this Rails app using the good old
rails new command.
$ rails new hello_world -T
You may be familiar with the
-T flag. This flag means “no tests”. If we had done
rails new hello_world without the
-T flag, we would have gotten a Rails application with a test directory containing MiniTest tests. I want to use RSpec, so I want us to start with no MiniTest.
Let’s also create our application’s database at this point since we’ll have to do that eventually anyway.
$ cd hello_world $ rails db:create
Update our Gemfile
This is the step where RSpec and Capybara will start to come into the picture. Each library will be included in our application in the form of a gem. We’ll also include the
webdrivers gem which is necessary in order for Capybara to interact with the browser.
Let’s add the following to our
Gemfile under the
:development, :test group. I’ve added a comment next to each gem or group of gems describing its role. Even with the comments, it may not be abundantly clear at this moment what each gem is for. At the end of the exercise we’ll take a step back and talk about which library enabled which step of what we just did.
group :development, :test do # The RSpec testing framework gem 'rspec-rails' # Capybara, the library that allows us to interact with the browser using Ruby gem 'capybara' # The following gems aids with the nuts and bolts # of interacting with the browser. gem 'webdrivers' end
Don’t forget to
Although we’ve already installed the RSpec gem, we haven’t installed RSpec into our application. Just like the Devise gem, for example, which requires us not only to add
devise to our
Gemfile but to also run
rails g devise:install, RSpec installation is a two-step process. After we run this command we’ll have a
spec directory in our application containing a couple config files.
$ rails g rspec:install
Now that we’ve gotten the “plumbing” work out of the way, let’s write some actual application code.
Creating our static page
Let’s generate a controller,
HelloController, with just a single action,
$ rails g controller hello_world index
Let’s modify the action’s template so it just says “Hello, world!”.
Just as a sanity check, let’s start the Rails server and open up our new page in the browser to make sure it works as expected.
$ rails server $ open http://localhost:3000/hello_world/index
We have our test infrastructure. We have the feature we’re going to test. Now let’s write the test itself.
Writing our test
Write the actual test
Let’s create a file called
spec/hello_world_spec.rb with the following content:
require 'rails_helper' RSpec.describe 'Hello world', type: :system do describe 'index page' do it 'shows the right content' do visit hello_world_index_path expect(page).to have_content('Hello, world!') end end end
If you focus on the “middle” two lines, the ones starting with visit and expect, you can see that this test takes the form of two steps: visit the hello world index path and verify that the page there says “Hello, world!” If you’d like to understand the test in more detail, below is an annotated version.
# This pulls in the config from spec/rails_helper.rb # that's needed in order for the test to run. require 'rails_helper' # RSpec.describe, or just describe, is how all RSpec tests start. # The 'Hello world' part is an arbitrary string and could have been anything. # In this case we have something extra in our describe block, type: :system. # The type: :system setting does have a functional purpose. It's what # triggers RSpec to bring Capybara into the picture when we run the test. RSpec.describe 'Hello world', type: :system do describe 'index page' do it 'shows the right content' do # This is where Capybara starts to come into the picture. "visit" is a # Capybara method. hello_world_index_path is just a Rails routing # helper method and has nothing to do with RSpec or Capybara. visit hello_world_index_path # The following is a mix of RSpec syntax and Capybara syntax. "expect" # and "to" are RSpec, "page" and "have_content" are Capybara. Newcomers # to RSpec and Capybara's English-sentence-like constructions often # have difficulty remembering when two words are separated by a dot or # an underscore or parenthesis, myself included. Don't worry, you'll # get familiar over time. expect(page).to have_content('Hello, world!') end end end
Now that we’ve written and broken down our test, let’s run it!
Run the test
This test can be run by simply typing
rspec followed by the path to the test file.
$ rspec spec/hello_world_spec.rb
The test should pass. There’s a problem with what we just did, though. How can we be sure that the test is testing what we think it’s testing? It’s possible that the test is passing because our code works. It’s also possible that the test is passing because we made a mistake in writing our test. This concern may seem remote but “false positives” happen a lot more than you might think. The only way to be sure our test is working is to see our test fail first.
Make the test fail
Why does seeing a test fail prior to passing give us confidence that the test works? What we’re doing is exercising two scenarios. In the first scenario, the application code is broken. In the second scenario, the application code is working correctly. If our test passes under that first scenario, the scenario where we know the application code is broken, then we know our test isn’t doing its job properly. A test should always fail when its feature is broken and always pass when its feature works. Let’s now break our “feature” by changing the text on our page to something wrong.
When we run our test now it should see it fail with an error message like
expected to find text "Hello, world!" in "Jello, world!".
$ rspec spec/hello_world_spec.rb
Watch the test run in the browser
Add the following to the bottom of
Capybara.default_driver = :selenium_chrome
Then run the test again:
$ rspec spec/hello_world_spec.rb
This time, Capybara should pop open a browser where you can see our test run. When I’m actively working with a test and I want to see exactly what it’s doing, I often find the running of the test to be too fast for me, so I’ll temporarily add a
sleep wherever I want my test to slow down. In this case I would put the
sleep right after visiting
require 'rails_helper' RSpec.describe 'Hello world', type: :system do describe 'index page' do it 'shows the right content' do visit hello_world_index_path sleep(5) expect(page).to have_content('Hello, world!') end end end
There we have it: the simplest possible Rails + RSpec + Capybara test. Unfortunately “the simplest possible Rails + RSpec + Capybara test” is still not particularly simple in absolute terms, but it’s pretty simple compared to everything that goes on in a real production application.