Lesson Tuesday

So far, we've focused on unit testing. As we add another layer of complexity to our programs with Sinatra, we will need to test how our code "units" work together when integrated. This next level of testing is called integration testing and forms the basis of how we approach TDD with Sinatra.

We'll use a gem called Capybara to write integration tests. Capybara runs a headless browser to mimic the actions a human user would perform when interacting with our pages. A headless browser is one that doesn't include a graphical user interface. When we test our specs with Capybara, all of the steps we list in the body of the test are followed as if we were manually performing them ourselves.

Let's start by updating our Gemfile to add Capybara. You may need to specify the Capybara version depending on your Ruby version. Ask an instructor to help if you run into errors.

We'll also add the gem Launchy to our Gemfile to handle automatically launching our application for debugging purposes. We'll learn more about debugging with Capybara at the end of this lesson.

Gemfile
source('https://rubygems.org')

gem('sinatra')      
gem('sinatra-contrib')
gem('rspec')
gem('capybara')
gem('launchy')
gem('pry')

Remember to run $ bundle install after updating a Gemfile.

Now we can begin the TDD process by using the spec file called album_integration_spec.rb that we originally created during the Sinatra Project Setup lesson. If you don't have that file, go ahead and create it now. Next, we'll write out our first integration spec and then discuss the code.

album_integration_spec.rb
require('capybara/rspec')
require('./app')
Capybara.app = Sinatra::Application
set(:show_exceptions, false)

describe('create an album path', {:type => :feature}) do
  it('creates an album and then goes to the album page') do
    visit('/albums')
    click_on('Add a new album')
    fill_in('album_name', :with => 'Yellow Submarine')
    click_on('Create!')
    expect(page).to have_content('Yellow Submarine')
  end
end

At the top, we require Capybara's support for RSpec and then we require our Sinatra app. Next, we tell Capybara that it will be testing our Sinatra application. There are many types of applications Capybara can test, not just Sinatra.

Finally, we have our test. The tests we write with Capybara are similar to the unit tests we've seen before with a few exceptions. The describe statement has a second argument with the hash {:type => :feature}, which is required for Capybara and RSpec to work together. Capybara also has a number of built-in methods like visit, fill-in, click_button and page which handle navigating through our application just as a user would.

In plain English, the test reads:

  • visit the '/albums' route and click_on "Add a new album";
  • fill_in the album's name with "Yellow Submarine" and click_on "Create!";
  • Finally, expect page to have the content "Yellow Submarine."

The assertion (expect(page).to have_content('XYZ')) just looks for one snippet of text on the page. It's important to choose a meaningful snippet that will be unique. For example, if the album's name is just "Album" and the page already has the content "Album of the Month," there could potentially be a false pass.

Our tests will be bundled with RSpec so we can run rspec and get a passing test.

Let's write a more complex integration test for further practice. This one will ensure that a user can add a song:

album_integration_spec.rb
describe('create a song path', {:type => :feature}) do
  it('creates a song and then goes to the album page') do
    album = Album.new("Yellow Submarine", nil)
    album.save
    visit("/albums/#{album.id}")
    fill_in('song_name', :with => 'All You Need Is Love')
    click_on('Add song')
    expect(page).to have_content('All You Need Is Love')
  end
end

Note that we manually create an album within our test. Capybara is just Ruby and there's no need to navigate through creating an album in our application since we've already written a test for it. We visit that album's page with the help of string interpolation and then fill in a song name and click on "Add song."

Ultimately, the test doesn't end up being too complicated after all — but it would involve a lot of redundancy if we didn't manually create that album.

Check out the documentation for Capybara on GitHub. There are also several Capybara cheat sheets available online, including this one, courtesy of Tomas D'Stefano. It lists Capybara's most common helpers and provides information on how Capybara navigates checkboxes, finds links and so on.

Debugging with Capybara


We can stop Capybara at any time using save_and_open_page, which works somewhat similarly to a binding.pry. Let's take a look at our first test again. A small error has been added:

album_integration_spec.rb
...

describe('create an album path', {:type => :feature}) do
  it('creates an album and then goes to the album page') do
    visit('/albums')
    click_on('Add a New Album')
    fill_in('album_name', :with => 'Yellow Submarine')
    click_on('Go!')
    expect(page).to have_content('Yellow Submarine')
  end
end

...

We'll get the following error when we run our tests:

create an album path creates an album and then goes to the album page
     Failure/Error: click_on('Add a New Album')

     Capybara::ElementNotFound:
       Unable to find link or button "Add a New Album"

Since Capybara can't find the "Add a New Album" link, we should add save_and_open_page before that line:

album_integration_spec.rb
...

describe('create an album path', {:type => :feature}) do
  it('creates an album and then goes to the album page') do
    visit('/albums')
    save_and_open_page    # New line here!
    click_on('Add a New Album')
    fill_in('album_name', :with => 'Yellow Submarine')
    click_on('Go!')
    expect(page).to have_content('Yellow Submarine')
  end
end

...

This will open the page we are expecting to see in the browser. If we take a look, we'll see that the link is actually "Add a new album" — not "Add a New Album." Yes, Capybara is case-sensitive. The default browser Capybara uses is Rack::Test, which does not render CSS or JavaScript, so any pages opened in the browser will be unstyled and look plain.

Note that save_and_open_page will generate HTML files in the root of your project. We recommend adding those files to a .gitignore file:

.gitignore
capybara-*.html

Since these files are prefixed with capybara-, we can use the wildcard operator * to represent anything after capybara- and before the .html file extension.

So, whenever there's an issue with an integration test, use save_and_open_page like binding.pry to debug.

Just as we have to write unit tests for all business logic, we're now expected to use integration testing with Capybara to test the rest of our Sinatra applications. Aim to cover every line of code in a Sinatra application, including those in app.rb and Ruby code contained in views.

At the same time, it's important to be careful about using too much integration testing. These tests will slow down your test suite considerably, especially in larger applications. For now, our integration tests won't be too complicated. Just make sure to write a separate test for each route in an application. It's also possible for a test to hit multiple routes. For example, our first test above tests the routing for GET /albums, GET /albums/new, and POST /albums.

Terminology


Capybara: A gem to write integration tests.

Headless browser: A program that mimic the actions a human user would perform when interacting with our pages. Headless browsers don't include a graphical interface.

Integration testing: Tests how all the parts of our application are integrated together. Specifically, we use integration tests here to test our routing and views.

Gemfile


Gemfile
source('https://rubygems.org')

gem('sinatra')      
gem('sinatra-contrib')
gem('rspec')
gem('capybara')
gem('pry')

Sample Integration Spec


album_integration_spec.rb
require('capybara/rspec')
require('./app')
Capybara.app = Sinatra::Application
set(:show_exceptions, false)

describe('create an album path', {:type => :feature}) do
  it('creates an album and then goes to the album page') do
    visit('/albums')
    click_on('Add a new album')
    fill_in('album_name', :with => 'Yellow Submarine')
    click_on('Add album')
    expect(page).to have_content('Yellow Submarine')
  end
end

We can use save_and_open_page to debug integration tests.

Documentation:

Lesson 28 of 37
Last updated August 7, 2022