Tutorial: Writing A First Rails System Test

in rails

Rails 5.1 added support for system tests, which is now the easiest method to test our apps. A system test performs its scenario in a real browser just like a user would. Let's write some to understand how the work.

The Application

To test we need some functionality. I created a small rails app that allows users to post messages anonymously, showing all messages to all users. The entire code for the project along with the test is available here:

https://github.com/ynonp/rails-system-test-tutorial

When it works it looks something like this:

See the Pen EvvYVM by Ynon Perek (@ynonp) on CodePen.

System tests access our system via the view, so it's useful to look at the main (and only) view our application has, in the file app/views/thewalls/show.html.erb:

<h1>The Wall - Message Board</h1>

<%= form_tag(thewall_path) do %>
  <label>
    Message Text
    <%= text_field_tag(:text) %>
  </label>
  <%= submit_tag %>
<% end %>

<%= link_to 'Delete All',  thewall_path,  method: :delete %>


<ul>
<% @messages.each do |msg| %>
  <li><%= msg[:text] %>
<% end %>
</ul>

The view provides a form to add new messages, a button to delete all messages, and a list of submitted messages. Our tests will probably include submitting messages in the form to see them in the list and deleting existing messages.

System Tests Configuration

The first file you should look into is test/application_system_test_case.rb. This file defines configuration shared by all our system tests. Here's the default implementation:

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
end

The configuration opens chrome browser in size 1400x1400 to run the tests in. Later in the post we'll see how to use other browsers.

A First Test

System tests are located in subdirectories of test/system. Each file name must end with a _test.rb suffix, for example the two test cases I created are named test/system/add_message_test.rb and test/system/clear_messages_test.rb. The test cases use minitest and capybara DSL so our test code accurately describes the actions a real user would perform:

# file: test/system/add_message_test.rb
require 'application_system_test_case'

class AddMessageTest < ApplicationSystemTestCase
  test "sending a message" do
    visit thewall_path
    fill_in 'text', with: 'hello world 1'
    find('input[type="submit"]').click

    assert_selector('ul li:last-child', text: 'hello world 1')
  end

  test "sending another message" do
    visit thewall_path
    fill_in 'text', with: 'hello world 2'
    find('input[type="submit"]').click

    assert_selector('ul li:last-child', text: 'hello world 2')
  end
end

Some capybara terms if you're not familiar with the tool:

  1. visit takes a url and open it in the browser. We can use all our existing route helpers.
  2. fill_in types some text in a text field. The first parameter is the "name" attribute of the text field.
  3. More complex lookups are performed with find. This function takes a CSS selector and returns the matching element. In the above test code we called click on the submit button to send the message.
  4. assert_selector asserts an element exists that matches the given selector and attributes. The above example verifies the last li in the list has our expected text.

The two tests in the test case are performed sequentially and on the same server, but in different DB transactions which is rolled back after each test. That's why you won't find the first message in the second test block. Each test case runs in its own DB world.

Other capybara commands are available in this [https://gist.github.com/zhengjia/428105](cheat sheet) and in the project's documentation.

A Second Test

By now you can probably know where we're going with the next test, so we can open the relevant code:

# file: test/system/clear_messages_test.rb

require 'application_system_test_case'

class ClearMessagesTest < ApplicationSystemTestCase
  test "Delete all messages" do
    visit thewall_path
    assert_selector('ul li', count: 1)
    find('a', :text => "Delete All").click

    assert_selector('ul li', count: 0)
  end
end

I used the same capybara commands to press "Delete All" button and see the messages disappear. But, where did that first message came from? It was not created in the test code nor in previous tests.

Fixtures

System tests have the same fixture functionality as other rails tests, and that's why we already had a message even before creating any. It was created in the fixtures file:

# file: test/fixtures/messages.yml

one:
  text: welcome home

Fixtures are put in the database automatically and remain there after each test transaction is rolled back. Use them to create initial state that is not directly related to a specific test. In real life I try not to use fixtures too much as I find them confusing when reading or maintaining the tests.

Headless Testing and Other Browsers

Remember the automatic configuration we got from rails? Well it works great because you probably have Chrome on your box, but eventually you may need to run the test in different browsers or on different machines.

It's easy to define other configurations by calling Capybara.register_driver. Here's my test/test_helper.rb code which defines a headless chrome browser session:

# file: test/test_helper.rb

require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "selenium/webdriver"

Capybara.register_driver :headless_chrome do |app|
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
    chromeOptions: { args: %w(headless disable-gpu) }
  )

  Capybara::Selenium::Driver.new app,
    browser: :chrome,
    desired_capabilities: capabilities
end

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

To use the headless chrome defines here, change your test configuration in application_system_test_case.rb as follows:

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :headless_chrome
end

Final Thoughts

Browser tests were always a bit quirky in Rails, mainly because they were not synchronised with the database. With system tests, capybara and the rails test server run in the same thread, which means we no longer need database cleanup strategies. Every test runs in its own transaction.

Another cool feature is failure screenshots. These are created in tmp/screenshots folder automatically when tests fail and help us analyze the problem.

As modern applications use more JavaScript and client-side logic to do their work, system tests will become invaluable part or our development work. With the new system tests integration in Rails, it should even be fun.

Comments