© Brady Somerville, Adam Gamble, Cloves Carneiro Jr and Rida Al Barazi 2020
B. Somerville et al.Beginning Rails 6https://doi.org/10.1007/978-1-4842-5716-6_16

16. Testing Your Application

Brady Somerville1 , Adam Gamble2, Cloves Carneiro Jr.3 and Rida Al Barazi4
(1)
Bowling Green, KY, USA
(2)
Gardendale, AL, USA
(3)
Hollywood, FL, USA
(4)
FONTHILL, ON, Canada
 

When an application is in its infancy, its developers may be able to keep a complete mental model of the whole system. Changes to the system are relatively easy to reason about, easy to implement, and easy to verify they’re working as intended.

However, most applications change over time. And as they grow, developers lose the ability to keep a complete mental model of the whole system. When that happens, bugs are introduced more frequently, as a seemingly innocuous change in one part of the application can have surprising and unintended consequences in another area of the code. Developers start to feel this burden; changes take longer to reason about, are harder to implement, and are more time-consuming to verify. Increasingly, it feels like every release introduces new bugs, and even bug fixes introduce new bugs.

When we start breaking our application more frequently, we get nervous about introducing changes. Perhaps we know how we want to add a new feature, but we deem it too risky and settle for compromises in code quality. For example, maybe instead of changing a critical code path to support a new feature, we’ll add an if statement to take another nearly identical but slightly different code path for our new feature, so that if we break something, at least it will only be the new feature we’re breaking.

These compromises in code quality—sometimes referred to as technical debt —are easier for now, but harder to deal with later; a future requirement may necessitate changing both of these critical code paths; perhaps at that point in the future, we’ll feel brave enough to do what we wanted in the first place. But if we were reluctant to change one critical piece of code, we probably won’t want to change two critical pieces of code. So maybe we’ll just tack on another and another and so on. Left unchecked, this technical debt grows until the application is nearly unmaintainable.

How can we, as developers, avoid this? Automated testing is one of the most important things we can do to improve the quality of our code, reduce the cost of change, and keep our software (relatively) bug-free. Rails (and the Ruby community at large) takes testing seriously. Not surprisingly, Rails goes out of its way to make testing hassle-free.

The basic idea of automated testing is simple: you write code that exercises your program and tests your assumptions. Instead of just opening a browser and adding a new user manually to check whether the process works, you write a test that automates the process—something repeatable. With a test in place, every time you modify the code that adds a new user, you can run the test to see if your change worked—and, more important, whether your seemingly innocuous change broke something else.

If you stop and think about it, you’re already testing your software. The problem is that you’re doing it manually, often in an ad hoc fashion. You may make a change to the way users log in, and then you try it in your browser. You make a change to the sign-up procedure, and then you take it for a spin. As your application grows in size, it becomes more and more difficult to manually test like this, and eventually you miss something important. Even if you’re not testing, you can be sure that your users are. After all, they’re the ones using the application in the wild, and they’ll find bugs you never knew existed. The best solution is to replace this sort of visual, ad hoc inspection with automatic checking.

Testing becomes increasingly important when you’re refactoring existing code. Refactoring is the process of improving the design of code without changing its behavior. The best way to refactor is with a test in place acting as a safety net. Because refactoring shouldn’t result in an observable change in behavior, it shouldn’t break your tests either. It’s easy, therefore, to see why many programmers won’t refactor without tests.

Given the importance placed on testing, it may seem odd that this book leaves a discussion of this until Chapter 16. Ideally, you should be writing tests as you go, never getting too far ahead without testing what you’ve written. But we decided that explaining how to test would be overwhelming while you were still learning the basics of Ruby and the Rails framework. Now that you have a good deal of knowledge under your belt, it’s time to tackle testing.

Note

If you need to get the code at the exact point where you finished Chapter 15, download the zip file from www.apress.com and extract it onto your computer.

How Rails Handles Testing

Because Rails is an integrated environment, it can make assumptions about the best ways to structure and organize your tests. Rails provides
  1. 1.

    Test directories for controller, model, mailer, helper, system, and integration tests (and more)

     
  2. 2.

    Fixtures for easily working with database data

     
  3. 3.

    An environment explicitly created for testing

     
The default Rails skeleton generated by the rails command creates a directory just for testing. If you open it, you’ll see subdirectories for each of the aforementioned test types:
test
 |-- channels          <-- Action Cable tests
 |-- controllers        <-- controller tests
 |-- fixtures             <-- test data
 |-- helpers             <-- helper tests
 |-- integration        <-- integration tests
 |-- jobs                  <-- Active Job tests
 |-- mailboxes        <-- Action Mailbox tests
 |-- mailers             <-- Action Mailer tests
 |-- models             <-- model tests
 |-- system             <-- system tests

Several of these directories should look familiar. You can probably imagine what types of things will be tested in the channels, controllers, helpers, jobs, mailboxes, mailers, and models directories. But a few directories probably won’t look familiar. Fixtures? Integration? System? What are these for? We’ll cover integration and system tests later in the chapter, but let’s take a quick look at fixtures now.

Fixtures are textual representations of table data written in YAML—a data serialization format. Fixtures are loaded into the database before your tests run; you use them to populate your database with data to test against. Look at the users fixtures file in test/fixtures/users.yml, as shown in Listing 16-1.
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
  email: MyString
  password: MyString
two:
  email: MyString
  password: MyString
Listing 16-1

Users Fixtures in test/fixtures/users.yml

Rails generated the users fixtures file for us when we generated the User model. As you can see, the file has two fixtures, named one and two. Each fixture has both attributes email and password set to MyString; but, as you recall, you renamed the password column hashed_password back in Chapter 6. Let’s update the users fixtures file to reflect the new column name and use meaningful data. Listing 11-2 shows the updated fixture.
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
eugene:
  email: eugene@example.com
  hashed_password: e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 # => secret
lauren:
  email: lauren@example.com
  hashed_password: e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 # => secret
Listing 16-2

Updated Users Fixtures in test/fixtures/users.yml: https://gist.github.com/nicedawg/393f21dc9e39a70be49b18970a8967ad

Remember that every time we generated a model or a controller while building the blog application, Rails automatically generated test files for us. This is another example of its opinionated nature—Rails thinks we should test, so it goes out of its way to remind us.

You may also recall that Rails created three SQLite databases for the blog application: one for development (which is all you’ve been using thus far), one for production, and one for testing. Not surprisingly, Rails uses the testing database just for testing.

Rails drops and re-creates this test database on every run of the test suite. Make sure you don’t list your development or production database in its place, or all your data will be gone.

Unit Testing Your Rails Application

You know that Rails generated some tests automatically. Let’s open one of them now and take a look. Let’s start with the Article test, located in test/models/article_test.rb, as shown in Listing 16-3.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #  assert true
  # end
end
Listing 16-3

Generated Article Unit Test in test/models/article_test.rb

Although there’s not much to it (all it does is show you how to test that true is, in fact, true), this test gives you a template from which to build your real tests. It has the following elements:
  1. 1.

    The test class is a subclass of ActiveSupport::TestCase, which is Rails’ enhanced version of the Minitest::Test class, which comes from the Ruby testing framework, minitest.

     
  2. 2.

    Tests are implemented as blocks using the test method, with the first parameter as the description of that test—"the truth" in this case.

     
  3. 3.

    Within a test case, assertions are used to test expectations. The “Testing with Assertions” section later in this chapter explains how these work.

     
If you peek inside the test/models directory, you’ll see a similar test case for every model we’ve generated so far: Article, Comment, Category, User, and Profile. Each looks almost exactly the same as the Article test. Let’s run the unit tests now using the rails test:models command from your command prompt and see what happens:
$ rails test:models
Run options: --seed 50142
# Running:
Finished in 0.140507s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

In this case, there are no tests yet (the ones generated are commented out). If there were tests and the test passed, you would see a . (dot) character. When the test case produces an error, you would see an E. If any assertion fails to return true, you would see an F. Finally, when the test suite is finished, it prints a summary.

Also, you may have noticed that your seed was different. By default, your tests are run with a random seed value so that your tests are run in a different order each time. That helps us avoid writing order-dependent tests by making sure each test can pass independently of which tests have run before.

Also, we ran rails test:models because we only want to run the tests within our test/models directory right now. We could run rails test to run most of our tests (more on that later) or rails test test/models/article_test.rb to run the tests in a specific file or even rails test test/models/article_test.rb:26 to run a specific test case within a file based on the provided line number. See rails test -h for more information.

Testing the Article Model

Let’s test the Article model. If you recall from Chapter 5, one of the first things you did with your Article model was basic CRUD operations. Well, testing that you can create, read, update, and delete articles is a great place to start. Here’s a quick summary of the specific things you test in this chapter:
  1. 1.

    Creating a new article

     
  2. 2.

    Finding an article

     
  3. 3.

    Updating an article

     
  4. 4.

    Destroying an article

     

Before you begin, you’ll need to create a few fixtures (remember that a fixture is a textual representation of test data).

Creating Fixtures

Let’s create a fixture for an article so we can test it. Open the test/fixtures/articles.yml file and replace its content with the code as shown in Listing 16-4.
welcome_to_rails:
  user: eugene
  title: "Welcome to Rails"
  published_at: <%= 3.days.ago %>
Listing 16-4

Articles Fixtures in test/fixtures/articles.yml: https://gist.github.com/nicedawg/27d1df26c994bbd0c7579498cd8d91e7

We named this fixture welcome_to_rails so our tests can refer to it clearly. We declared eugene (a users fixture we added in the previous step) as the owner of the article, gave it a title, and used a little ERb to set the published_at datetime to 3 days ago. Our article still needs a body, but we can’t declare it here; you may remember that when we added Action Text in Chapter 11, the body attribute was moved to Action Text’s storage, so we’ll need to modify our ActionText::RichText fixtures file, found in test/fixtures/action_text/rich_texts.yml so that it matches Listing 16-5.
welcome_to_rails_body:
  record: welcome_to_rails (Article)
  name: body
  body:  <p>Rails is such a nice web framework written in ruby</p>
Listing 16-5

Action Text Fixtures in test/fixtures/action_text/rich_texts.yml: https://gist.github.com/nicedawg/210608fc3d5730b49032c73ce1d500fe

Here, we named this fixture welcome_to_rails_body, because that accurately defines this fixture. Its record value is a special syntax that identifies the type (Article) and id (welcome_to_rails) of the record to which this rich text belongs. The name indicates which attribute of the parent record it belongs to, and the body value declares the contents of this Action Text record. This fixture is a little more complicated than the previous ones, because it’s a polymorphic record, as you might remember from Chapter 11, but even then, fixtures still make it easy to work with.

The data in our fixtures files will be inserted automatically into our test database before our tests run. With our fixtures in place, we’re ready to start creating test cases!

Tip

Fixtures are parsed by ERb before they’re loaded, so you can use ERb in them just as you can in view templates. This is useful for creating dynamic dates, as we did in published_at: <%= 3.days.ago %>.

The following sections present the test cases one at a time, beginning with create.

Adding a Create Test

Open the test/models/article_test.rb file and create the first test case by deleting the test "the truth" method and replacing it with a test called test "should create article." Your file should look like Listing 16-6.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test 'should create article' do
    article = Article.new
    article.user = users(:eugene)
    article.title = 'Test Article'
    article.body = 'Test body'
    assert article.save
  end
end
Listing 16-6

The Create Article Test in test/models/article_test.rb: https://gist.github.com/nicedawg/e0d35b3317dbd1040ad93688f8db605d

The "should create article" test case is standard article creation fare. We created a new article in the same way we would create one from the console. The only real difference is on the last line of the test case—assert article.save. We know that article.save will return true if the save was successful and that it will return false if the save failed. assert is a method available to us in our tests which will mark the test as successful if given a true value and mark the test as failed if given a false value. Therefore, if the article saves successfully, the test passes; otherwise, it fails.

Note

Fixtures can be accessed in your test cases by name. Use fixture(:name), where fixture is the plural name of the model and :name is the symbolized name of the fixture you’re after. This returns an Active Record object on which you can call methods. Here, you get at the eugene users fixture using users(:eugene).

Let’s run our new test to see if it passes:
$ rails test:models
Run options: --seed 13876
# Running:
.
Finished in 0.285145s, 3.5070 runs/s, 3.5070 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Just as the output from the test says, you ran one test (test "should create article"), which included one assertion (assert article.save), and everything passed. Life is good!

A best practice is to test your tests. That may sound silly, but it’s easy to write a test that gives you a false positive. (This is why some developers prefer to write failing tests before writing the code that makes them pass.) A quick check for the test we just wrote would be to comment out the line in our test where we set the article’s title; since the article requires a title, the test should fail. Give it a try!

The assert method is one of many assertion methods available to us. Before we go any further, let’s take a closer look at assertions as they pertain to minitest and ActiveSupport::TestCase.

Testing with Assertions

Assertions are statements of expected outcome. If the assertion turns out to be correct, the assertion passes. If the assertion turns out to be false, the assertion fails, and minitest reports a failure.

While one could get by with only the assert method, many more assertion methods are available to us for convenience; minitest ships with a bevy of built-in assertions, and Rails adds some of its own. First, let’s look at some of the most commonly used minitest assertions as shown in Table 16-1.
Table 16-1

Standard minitest Assertion Methods

Assertion Method

Description

assert(test, msg = nil)

Fails unless test is truthy.

assert_empty(object, msg = nil)

Fails unless object is empty.

assert_equal(expected, actual, msg = nil)

Fails unless exp == act.

assert_in_delta(expected_float, actual_float, delta = 0.001, msg = nil)

Fails unless expected_float is within delta of actual_float.

assert_includes(collection, object, msg = nil)

Fails unless collection includes object.

assert_instance_of(klass, object, msg = nil)

Fails unless object is an instance of the klass class.

assert_kind_of(klass, object, msg = nil)

Fails unless object is a kind of klass. (Note: Unlike assert_instance_of, which checks the object’s class directly, assert_kind_of considers the object's ancestors.)

assert_match(matcher, object, msg = nil)

Fails unless matcher =~ object.

assert_nil(object, msg = nil)

Fails unless object is nil.

assert_raises(exception_class, msg) do

  ...

end

Fails unless block raises an exception of type exception_class.

assert_respond_to(object, method, msg = nil)

Fails unless object responds to a method named method.

As you might have noticed, most of these assertion methods support an optional parameter to supply a custom failure message if desired. Including a custom failure message in most cases is not necessary, but it may occasionally be helpful.

Also, note that some methods show a klass argument. Why the misspelling? In Ruby, class is a keyword; when using a variable to store a reference to a class, we can’t use class as the variable name; it’s common practice to use the name klass.

Minitest also provides refute variations of these methods, which simply inverse the logic of the corresponding assertion. For example, refute_empty will pass when the provided object isn't empty. However, for backward compatibility with test-unit, Rails’ previous built-in testing framework (and arguably for readability), Rails provides aliases for these refute variations such as these: assert_not_empty, assert_not_equal, assert_no_match, and so on.

While we showed some of the most common assertion methods, there are more. When you find that an assertion is overly complicated and hard to read, it may be a good idea to check https://guides.rubyonrails.org/testing.html#rails-meets-minitest to see if perhaps a different assertion method would make your test more readable.

Adding a Find Test

Now that we know more about assertion methods, we’re ready to add more tests. Next on the list is testing that we can successfully find an article. We’ll use the data in the fixture we created to help us. Add the method shown in Listing 16-7 after the "should create article" test.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test 'should create article' do
    article = Article.new
    article.user = users(:eugene)
    article.title = 'Test Article'
    article.body = 'Test body'
    assert article.save
  end
  test 'should find article' do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end
end
Listing 16-7

Test Case for Finding an Article in  test/models/article_test.rb: https://gist.github.com/nicedawg/71fbefeec56ce2ada47e956d3115b243

Our new test verifies that we can find an article of the given id. First, we grab the id attribute from the fixture, and then we test that we can use Article.find to retrieve it. We use the assertion assert_nothing_raised because we know that find raises an exception if the record can’t be found. If no exception is raised, we know that finding works. Again, run the test and see what happens:
$ rails test test/models
Run options: --seed 22750
# Running:
..
Finished in 0.249469s, 8.0170 runs/s, 4.0085 assertions/s.
2 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Sure enough, finding works! So far, so good.

Adding an Update Test

Next, let’s test updating an article. Add the test "should update article" case, as shown in Listing 16-8.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test 'should create article' do
    article = Article.new
    article.user = users(:eugene)
    article.title = 'Test Article'
    article.body = 'Test body'
    assert article.save
  end
  test 'should find article' do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end
  test 'should update article' do
    article = articles(:welcome_to_rails)
    article.update(title: 'New title')
    assert_equal 'New title', article.reload.title
  end
end
Listing 16-8

Test Case for Updating an Article in test/models/article_test.rb: https://gist.github.com/nicedawg/24dc226151646294245b7c1f2380dd0b

First, we find the “Welcome to Rails” article from our fixtures file, and then we update the article with a new title and assert that when the article is reloaded, it has the new title. Once again, run the test and see what happens:
$ rails test:models
Run options: --seed 25358
# Running:
...
Finished in 0.270023s, 11.1102 runs/s, 7.4068 assertions/s.
3 runs, 2 assertions, 0 failures, 0 errors, 0 skips

Adding a Destroy Test

Only one more test to go: destroy. We’ll find an article, destroy it, and assert that Active Record raises an exception when you try to find it again. Listing 16-9 shows the test.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test 'should create article' do
    article = Article.new
    article.user = users(:eugene)
    article.title = 'Test Article'
    article.body = 'Test body'
    assert article.save
  end
  test 'should find article' do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end
  test 'should update article' do
    article = articles(:welcome_to_rails)
    article.update(title: 'New title')
    assert_equal 'New title', article.reload.title
  end
  test 'should destroy article' do
    article = articles(:welcome_to_rails)
    article.destroy
    assert_raise(ActiveRecord::RecordNotFound) { Article.find(article.id) }
  end
end
Listing 16-9

Test Case for Destroying an Article in test/models/article_test.rb: https://gist.github.com/nicedawg/813a3e5f9bf5d3fdbfe888a2dc4e3360

The assert_raise assertion takes as an argument the class of the exception you expect to be raised for whatever you do inside the given block. Because you’ve deleted the article, you expect Active Record to respond with a RecordNotFound exception when you try to find the article you just deleted by id. Run the test and see what happens:
$ rails test test/models
Run options: --seed 26110
# Running:
....
Finished in 0.275000s, 14.5455 runs/s, 10.9091 assertions/s.
4 runs, 3 assertions, 0 failures, 0 errors, 0 skips

We’ve done it! We’ve successfully tested the happy path for each CRUD operation for our Article model.

Testing Validations

We have a few validations on our Article model, specifically for the presence of a title and body. Because we want to make sure these are working as expected, we should test them too. Let’s add the method shown in Listing 16-10 to our test to prove that we can’t create invalid articles.
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test 'should create article' do
    article = Article.new
    article.user = users(:eugene)
    article.title = 'Test Article'
    article.body = 'Test body'
    assert article.save
  end
  test 'should find article' do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end
  test 'should update article' do
    article = articles(:welcome_to_rails)
    article.update(title: 'New title')
    assert_equal 'New title', article.reload.title
  end
  test 'should destroy article' do
    article = articles(:welcome_to_rails)
    article.destroy
    assert_raise(ActiveRecord::RecordNotFound) { Article.find(article.id) }
  end
  test 'should not create an article without title nor body' do
    article = Article.new
    assert !article.save
    assert_not_empty article.errors[:title]
    assert_not_empty article.errors[:body]
    assert_equal ["can't be blank"], article.errors[:title]
    assert_equal ["can't be blank"], article.errors[:body]
  end
end
Listing 16-10

Test Case for Validations in test/models/article_test.rb: https://gist.github.com/nicedawg/603e64d1846086488873757624745245

This is pretty straightforward, although you may have to read it a few times before it clicks. First, we instantiate a new Article object in the local variable article. Without having given it any attributes, we expect it to be invalid, so we assert that article.save returns false. (Notice the !, which negates truth). Next, we access the errors hash to explicitly check for the attributes we expect to be invalid:
assert_not_empty article.errors[:title]
assert_not_empty article.errors[:body]
We also want to check that the validation responses are what we expect. To do this, we use the assert_equal assertion. Here’s its basic syntax:
assert_equal(expected, actual)
To check the error messages, we again access the errors hash, but this time we ask for the specific messages associated with the given attribute:
    assert_equal ["can't be blank"], article.errors[:title]
    assert_equal ["can't be blank"], article.errors[:body]
Finally, we assert that article.save returns false using !article.save. Run the test one more time:
$ rails test:models
Run options: --seed 28498
# Running:
.....
Finished in 0.335649s, 14.8965 runs/s, 32.7723 assertions/s.
5 runs, 11 assertions, 0 failures, 0 errors, 0 skips

Feels good, doesn’t it? Our tests pass now, but any application being used is likely to change as requirements change. What if one day we decide to make a change to the Article model and remove the validation requirements for the title attribute? If that were to happen, our test would fail. If you want to try it, open the Article model in app/models/article.rb and remove :title from the validates line which checks for presence, and then run the tests again.

When our requirements change, we often need to update our tests. We recommend updating the tests first (which should make them fail) and then updating the code (which makes them pass). This is also known as test-driven development (TDD).

Though we added several tests for our Article model, we certainly didn’t test everything that the Article model can do—nor did we write unit tests for our other models—but hopefully we’ve gained a good understanding of how to write unit tests for our models. Next, we’ll learn how to test another critical component of our application—our controllers.

Functional Testing Your Controllers

Tests to specifically check your controllers are called functional tests. When we tested our models, we didn’t test them in the context of the web application—there were no web requests and responses, nor were there any URLs to contend with. This focused approach lets you hone in on the specific functionality of the model and test it in isolation. Alas, Rails is great for building web applications; and although unit testing models is important, it’s equally important to test the full request/response cycle.

Testing the Articles Controller

Functional tests aren’t that much different from unit tests. The main difference is that Rails sets up request and response objects for us; these objects act just like the live requests and responses we get when running the application via a web server. If we open the articles controller test in test/controllers/articles_controller_test.rb (which Rails generated for us when we scaffolded the articles controller in an earlier chapter) and examine the first few lines, as shown in Listing 16-11, we can see how this is done.
require 'test_helper'
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # ...
end
Listing 16-11

Setup of a Controller Test in test/controllers/articles_controller_test.rb

Just as in the unit test we worked with earlier, the first thing we do is require test_helper. The test_helper.rb file sets up some common environment variables and generally endows minitest with specific methods that make testing Rails applications easier.

Note

You can think of test_helper as being akin to application_helper. Any methods you define here are available to all your tests.

Notice that ArticlesControllerTest is a subclass of ActionDispatch::IntegrationTest, which performs some magic for us behind the scenes. It gives our tests the ability to send HTTP requests to our controller, make assertions against the response from the controller, and make assertions on the cookies, flash, and session hashes which our controller action may have modified. It also prepares three instance variables for us to use in our tests: the first is @controller as an instance variable of ArticlesController, after which it instantiates both @request and @response variables, which are instances of ActionDispatch::Request and ActionDispatch::TestResponse, respectively.

Most of the time, we don’t need to worry about all this. Still, it’s important to know what’s going on. Because the test we’re looking at was created by the scaffold generator, it has quite a bit more code than we would get from the standard controller generator. There’s a problem with this code, though: these test cases will not pass—at least not without some modification. Warts and all, this gives us a good start and serves well as a template.

As you look over the articles controller test file, notice that each test case tests a specific request for an action on the controller. There’s a test for every action: index, show, new, create, edit, update, and destroy. Let’s walk through each test case, making adjustments as we go.

Creating a Test Helper Method

Before we start testing our ArticlesController actions, we realize that in order to create an article, our application expects a logged-in user. In fact, it’s conceivable that many of our tests may expect a logged-in user. This is a perfect job for a test helper, because it can be shared across many tests. We’ll create a helper method called login_as that accepts a user’s name and makes the necessary request to log them in. We can use this method for any test case that requires a login.

While we’re editing our test_helper file, we’ll go ahead and include the Turbolinks::Assertions module into the ActionDispatch::IntegrationTest; doing so will make sure that when we make assertions about being redirected in response to a Turbolinks request, our tests will still work seamlessly.

To begin, open test/test_helper.rb in your editor and make the highlighted changes, as shown in Listing 16-12.
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)
  # 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...
  ActionDispatch::IntegrationTest.include Turbolinks::Assertions
  def login_as(user)
    post session_url(email: users(user).email, password: 'secret')
  end
end
Listing 16-12

The login_as Test Helper in test/test_helper.rb: https://gist.github.com/nicedawg/304592e83622cafa9297747d40dcab1f

The login_as method is pretty simple. It simply takes the provided fixture id for a user and sends a POST request to session_url—just like a real user of our app would—with their email address and password.

Now that we’ve created a handy shortcut to log in a user during our tests and included the Turbolinks::Assertions module, we’re ready to proceed with our tests, beginning with the index action.

Getting ArticlesControllerTest to Pass

Since we changed the id of our articles fixture in a previous step, we need to update the setup method in our test. (If we tried to run the test without making this change, every single test case in this file would fail with an error like “No fixture named ‘one’ found for fixture set ‘articles.’”) Modify your test/controllers/articles_controller_test.rb so that it matches Listing 16-13.
require 'test_helper'
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @article = articles(:welcome_to_rails)
  end
  test "should get index" do
    get articles_url
    assert_response :success
  end
  test "should get new" do
    get new_article_url
    assert_response :success
  end
  test "should create article" do
    assert_difference('Article.count') do
      post articles_url, params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    end
    assert_redirected_to article_url(Article.last)
  end
  test "should show article" do
    get article_url(@article)
    assert_response :success
  end
  test "should get edit" do
    get edit_article_url(@article)
    assert_response :success
  end
  test "should update article" do
    patch article_url(@article), params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    assert_redirected_to article_url(@article)
  end
  test "should destroy article" do
    assert_difference('Article.count', -1) do
      delete article_url(@article)
    end
    assert_redirected_to articles_url
  end
end
Listing 16-13

Updated Setup for test/controllers/articles_controller_test.rb: https://gist.github.com/nicedawg/b3c1492071238170aca06160f4a5061c

The setup method is executed before every test case. In this case, the setup method assigns the :welcome_to_rails article record from the fixtures to an instance variable @article; the @article variable is available to all test cases in the ArticlesControllerTest class.

Our controller tests define methods that correspond to HTTP verbs (get, post, patch, and delete) and provide our route helpers, which we can use to make requests. The first line of the "should get index" test makes a GET request for the index action using get articles_url. Here’s the full syntax you use for these requests:
http_method(path, parameters, headers, env, xhr, as)

In the case of the "should get index" test, we have no parameters to submit along with the request, so our call is simple. It makes a GET request to the index action just as if you had done so with a browser. Try looking at your log/test.log file as you run a controller test; you should see output that looks just like your server output when using your application with a browser in development mode.

After the request has been made, we use assert_response :success to prove that the request had a successful HTTP response code.

The assert_response assertion is a custom assertion defined by Rails (i.e., it’s not part of the standard minitest library), and it does exactly what its name implies: it asserts that the actual response status matches the expected status.

Every time you make an HTTP request, the server responds with a status code. When the response is successful, the server returns a status code of 200. When an error occurs, it returns 500. And when the browser can’t find the resource being requested, it returns 404. In our assertion, we used the shortcut :success, which is the same as 200. We could have used assert_response(200), but it’s easier to remember words like success or error than HTTP status codes, which is why we avoid using the numeric codes whenever possible. Table 16-2 lists the shortcuts available when using assert_response.
Table 16-2

Status Code Shortcuts Known to assert_response

Symbol

Meaning

:success

Status code was 200.

:redirect

Status code was in the 300–399 range.

:missing

Status code was 404.

:error

Status code was in the 500–599 range.

Tip

You can pass an explicit status code number to assert_response, such as assert_response(501) or its symbolic equivalent assert_response(:not_implemented). See https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml for the full list of codes and default messages you can use.

Let’s run the test for the index action. (Note: This command assumes your code matches the preceding listing. If not, you may need to adjust the line number used in the command to ensure you’re running the right test.)
$ rails test test/controllers/articles_controller_test.rb:8
Run options: --seed 30227
# Running:
.
Finished in 0.559850s, 1.7862 runs/s, 1.7862 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Good! Our index action test case passes. It’s true that all we tested was the status code of the response; we didn’t test that the correct content was included in the response or the right view templates were rendered, for example. This used to be possible with Rails out of the box, but Rails 5 removed this from controller tests, because they felt that controller tests should be focused on the effects of executing a particular controller action, not the details of how it does it.

So why would we write a controller test if we’re going to write another kind of test that also runs our controller actions, but with richer tools for assertions? Well, often we might not. But as you gain experience writing more tests, you’ll see there are always trade-offs involved; for example, tests which use an actual browser to more accurately simulate a user’s experience may be a more accurate test of your system as a whole, but those tests are magnitudes slower than a controller test.

A common strategy is to use faster tests to cover the full range of all possible scenarios in your application while using slower (but more thorough) tests to cover your application’s critical paths.

It is possible, by adding the rails-controller-testing to our project, to add the ability to assert instance variable assignments and which templates were rendered back into our controller tests, but we’ll follow the recommendations of the Rails team; besides, we’ll learn later in this chapter how to write other types of tests which will give us richer tools for verifying the content of a response.

Okay, back to our tests. Let’s run the entire ArticlesControllerTest and see where we stand:
$ rails test test/controllers/articles_controller_test.rb
Run options: --seed 8758
# Running:
F
Failure:
ArticlesControllerTest#test_should_get_new [/Users/brady/Sites/beginning-rails-6-blog/test/controllers/articles_controller_test.rb:15]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login>
Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html>
rails test test/controllers/articles_controller_test.rb:13
... more failures omitted ...
Finished in 0.483794s, 14.4690 runs/s, 16.5360 assertions/s.
7 runs, 8 assertions, 5 failures, 0 errors, 0 skips

Oh my. Out of our seven test cases, five failed. Don’t worry; often, there’s a single root cause which, when fixed, can clear up multiple test cases.

Looking at the preceding failure output, we see that the “should get new” test case failed because we expected it to be successful, but instead it redirected to the login page. Of course! We only allow logged-in users to access the new article form, to create an article, to edit an article, to update an article, and to destroy an article!

So let’s use the login_as test helper method we created where it’s needed and see if our tests will now pass. Modify test/controllers/articles_controller.rb to match Listing 16-14.
require 'test_helper'
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @article = articles(:welcome_to_rails)
  end
  test "should get index" do
    get articles_url
    assert_response :success
  end
  test "should get new" do
    login_as :eugene
    get new_article_url
    assert_response :success
  end
  test "should create article" do
    login_as :eugene
    assert_difference('Article.count') do
      post articles_url, params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    end
    assert_redirected_to article_url(Article.last)
  end
  test "should show article" do
    get article_url(@article)
    assert_response :success
  end
  test "should get edit" do
    login_as :eugene
    get edit_article_url(@article)
    assert_response :success
  end
  test "should update article" do
    login_as :eugene
    patch article_url(@article), params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    assert_redirected_to article_url(@article)
  end
  test "should destroy article" do
    login_as :eugene
    assert_difference('Article.count', -1) do
      delete article_url(@article)
    end
    assert_redirected_to articles_url
  end
end
Listing 16-14

Adding login_as to Test Cases in ArticlesControllerTest: https://gist.github.com/nicedawg/ec58c6d0b771078a308580fbc2c1743c

Let’s run our test again and see if we made a dent in our test failures:
$ rails test test/controllers/articles_controller_test.rb
Run options: --seed 42474
# Running:
.......
Finished in 0.675501s, 10.3627 runs/s, 13.3234 assertions/s.
7 runs, 9 assertions, 0 failures, 0 errors, 0 skips

Hurrah! Our tests pass now! We could declare victory and end on a high note, but we realize that these tests were supplied by our scaffolding; while they’re very useful to have, they only cover the happy path—that is to say, they only make assertions on what we think of as typical interactions.

Handling Edge Cases

However, we often want our tests to cover edge cases. For instance, we only allow the owner of an article to edit the article. We don’t allow just any logged-in user to edit any article. To be sure we don’t accidentally remove this security restriction, we should write tests that cover the scenario of user A trying to edit user B’s article.

Let’s add tests to prove that logged-in users who are not the owner are not allowed to edit, update, or destroy another’s article. Let’s modify ArticlesControllerTest in test/controllers/articles_controller_test.rb to match Listing 16-15.
require 'test_helper'
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @article = articles(:welcome_to_rails)
  end
  test "should get index" do
    get articles_url
    assert_response :success
  end
  test "should get new" do
    login_as :eugene
    get new_article_url
    assert_response :success
  end
  test "should create article" do
    login_as :eugene
    assert_difference('Article.count') do
      post articles_url, params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    end
    assert_redirected_to article_url(Article.last)
  end
  test "should show article" do
    get article_url(@article)
    assert_response :success
  end
  test "should get edit" do
    login_as :eugene
    get edit_article_url(@article)
    assert_response :success
  end
  test "should raise RecordNotFound when non-owner tries to get edit" do
    login_as :lauren
    assert_raises(ActiveRecord::RecordNotFound) do
      get edit_article_url(@article)
    end
  end
  test "should update article" do
    login_as :eugene
    patch article_url(@article), params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    assert_redirected_to article_url(@article)
  end
  test "should raise RecordNotFound when non-owner tries to update article" do
    login_as :lauren
    assert_raises(ActiveRecord::RecordNotFound) do
      patch article_url(@article), params: { article: { body: @article.body, excerpt: @article.excerpt, location: @article.location, published_at: @article.published_at, title: @article.title } }
    end
  end
  test "should destroy article" do
    login_as :eugene
    assert_difference('Article.count', -1) do
      delete article_url(@article)
    end
    assert_redirected_to articles_url
  end
  test "should raise RecordNotFound when non-owner tries to destroy article" do
    login_as :lauren
    assert_raises(ActiveRecord::RecordNotFound) do
      delete article_url(@article)
    end
  end
end
Listing 16-15

Handling Security in ArticlesControllerTest https://gist.github.com/nicedawg/36039c7dd6feed0883c526a924e712d7

As you can see, we added additional tests next to our existing edit, update, and destroy tests which assert that an exception is raised when a user attempts to modify an article which they don't own. In each case, we logged in as Lauren (because we know our articles fixture is owned by Eugene) and attempted the edit/update/delete operation which Eugene was able to successfully complete, but verified that Lauren was unable to do so. Very powerful!

Looking at our articles controller, it seems our controller test has covered most of its functionality; however, we realize we haven’t yet covered the notify_friend action. Let’s modify our ArticlesControllerTest in tests/controllers/articles_controller_test.rb to add a couple of tests to cover the scenarios when a reader submits the “Email a Friend” form with both valid and invalid information, as shown in Listing 16-16.
require 'test_helper'
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @article = articles(:welcome_to_rails)
  end
  # ... code omitted for brevity ...
  test "should raise RecordNotFound when non-owner tries to destroy article" do
    login_as :lauren
    assert_raises(ActiveRecord::RecordNotFound) do
      delete article_url(@article)
    end
  end
  test "should redirect to article url when submitting valid email a friend form" do
    post notify_friend_article_url(@article), params: {
      email_a_friend: { name: 'Joe', email: 'joe@example.com' }
    }, xhr: true
    assert_redirected_to article_url(@article)
  end
  test "should respond with unprocessable_entity when submitting invalid email a friend form" do
    post notify_friend_article_url(@article), params: {
      email_a_friend: { name: 'Joe', email: 'notAnEmail' }
    }, xhr: true
    assert_response :unprocessable_entity
  end
end
Listing 16-16

Covering notify_friend in ArticlesControllerTest https://gist.github.com/nicedawg/b1da893b01ff4e3e3eff5778ef8b912f

The first test proves that when a valid “Email a Friend” form submission is sent to the ArticlesController, the response is a redirect to the article show page. And the next test proves that when invalid data is sent, an HTTP status code for “unprocessable entity” is returned. Note that we supplied an additional option to our post calls which we haven’t used yet—xhr: true. We know that these requests to submit the “Email a Friend” form use Ajax, so we set xhr: true so that the request will indicate it expects a JavaScript response.

Now that we’ve added these test cases, let’s run the entire controller test again and see what happens:
$ rails test test/controllers/articles_controller_test.rb
Run options: --seed 9405
# Running:
F
Failure:
ArticlesControllerTest#test_should_redirect_to_article_url_when_submitting_valid_email_a_friend_form [/Users/brady/Sites/beginning-rails-6-blog/test/controllers/articles_controller_test.rb:77]:
Expected response to be a Turbolinks visit to <http://www.example.com/articles/517600287> but was a visit to <http://www.example.com/login>.
Expected "http://www.example.com/articles/517600287" to be === "http://www.example.com/login".
rails test test/controllers/articles_controller_test.rb:73
F
Failure:
ArticlesControllerTest#test_should_respond_with_unprocessable_entity_when_submitting_invalid_email_a_friend_form [/Users/brady/Sites/beginning-rails-6-blog/test/controllers/articles_controller_test.rb:84]:
Expected response to be a <422: unprocessable_entity>, but was a <200: OK>
Response body: Turbolinks.clearCache()
Turbolinks.visit("http://www.example.com/login", {"action":"replace"}).
Expected: 422
  Actual: 200
rails test test/controllers/articles_controller_test.rb:80
..........
Finished in 0.763068s, 15.7260 runs/s, 20.9680 assertions/s.
12 runs, 16 assertions, 2 failures, 0 errors, 0 skips

Hmm. Our new tests failed. Looking closely at the failure output, we see both responses redirected to the login path. What? We don’t require a visitor to be logged in to send an email to a friend, do we?

In fact, we do! This was clearly an oversight on our part (perhaps you realized this earlier). Looking at the top of ArticlesController, we require authentication for all actions except for index and show. When we added the notify_friend action in a previous chapter, we should have also excluded notify_friend from requiring authentication. Not only can tests help prevent future bugs, they can help us find current bugs! Let’s fix this bug in our application by modifying ArticlesController in app/controllers/articles_controller.rb to match Listing 16-17.
class ArticlesController < ApplicationController
  before_action :authenticate, except: [:index, :show, :notify_friend]
  before_action :set_article, only: [:show, :notify_friend]
  # ... code omitted for brevity ...
end
Listing 16-17

Fix Bug in ArticlesController Which Required Authentication to Send Email to a Friend https://gist.github.com/nicedawg/68d8c626d3f3dd9cf37d6bdb56c93f87

Run the test again, and we should see that our ArticlesController now passes all of its tests.

In case you’re still on the fence about whether writing automated tests is worth the effort, try running this entire test file again and observe how long it takes to run; quite likely, it takes less than one second. Consider how long it would take to verify these scenarios manually—a few minutes at best. (And if you’re like me, you might forget to manually verify one or two of them.) Also consider the fact that writing tests helped us find an embarrassing bug!

We’ve covered some of the most common scenarios when writing controller tests, but testing is a deep topic. When you’re ready for more information, the Rails guide at https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers is a great resource.

Next, we’ll take a look at other types of tests we can write.

Running the “Full” Test Suite

Up until this point, we’ve been using the rails test command with arguments, in order to run certain tests. But we mentioned that if we run the rails test command with no arguments, it runs most of the tests. (It will exclude any system tests, which we’ll describe later.)

So let’s try running the rails test command and see if we have any more broken tests:
$ rails test
Run options: --seed 40931
# Running:
.........E
Error:
NotifierMailerTest#test_email_friend:
ArgumentError: wrong number of arguments (given 0, expected 3)
    app/mailers/notifier_mailer.rb:2:in `email_friend'
    test/mailers/notifier_mailer_test.rb:6:in `block in <class:NotifierMailerTest>'
rails test test/mailers/notifier_mailer_test.rb:4
.....E
Error:
DraftArticlesMailerTest#test_no_author:
ArgumentError: wrong number of arguments (given 0, expected 1)
    app/mailers/draft_articles_mailer.rb:8:in `no_author'
    test/mailers/draft_articles_mailer_test.rb:6:in `block in <class:DraftArticlesMailerTest>'
rails test test/mailers/draft_articles_mailer_test.rb:4
...
Finished in 0.704911s, 26.9538 runs/s, 38.3027 assertions/s.
19 runs, 27 assertions, 0 failures, 2 errors, 0 skips

Looking closely at the failures, we see we have a couple of mailer tests which need attention. That makes sense; we used the Rails generator to create these mailer classes, but never updated their tests to reflect the changes we made. Let’s fix these mailer tests.

Mailer Tests

Mailers can be just an integral part of your application as controllers and models and deserve to be tested as well. Again, Rails gives us tools to make doing so as easy as possible.

We saw in the previous section that we have some failing mailer tests, since we didn’t update the generated tests when we updated our mailers, so let’s fix that.

First, let’s focus on the failure from our NotifierMailerTest. Looking at the test (in test/mailers/notifier_mailer_test.rb) and the mailer (in app/mailers/notifier_mailer.rb) side by side, we see that the problem is that in the test, we’re not passing any parameters to the email_friend method. We also see that our test’s assertions are no longer valid. Let’s edit our NotifierMailerTest to fix the test, as shown in Listing 16-18.
require 'test_helper'
class NotifierMailerTest < ActionMailer::TestCase
  def setup
    @article = articles(:welcome_to_rails)
    @sender_name = 'Reed'
    @receiver_email = 'to@example.com'
  end
  test "email_friend" do
    mail = NotifierMailer.email_friend(@article, @sender_name, @receiver_email)
    assert_emails 1 do
      mail.deliver_now
    end
    assert_equal "Interesting Article", mail.subject
    assert_equal ["to@example.com"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Your friend, <em>#{@sender_name}</em>", mail.body.encoded
    assert_match @article.title, mail.body.encoded
  end
end

First, we added a setup method to run before the test, to fetch the article and set the sender and receiver information. We could have done this in the test itself, but this helps keep the test clearer and encourages reuse for future tests.

Then, we passed the parameters that NotifierMailer.email_friend expects. This fixes the immediate problem our previous test run showed us, but we made some more changes.

After that, we used a special assertion method—assert_emails—which Action Mailer provides to prove that if we call deliver_now on the mailer object, one email is “delivered.”

Then, we updated the existing assertions to match what we expect the email to look like. We could have chosen to ensure that every character in the body of the email is exactly what we expect, but we decided to just check for the critical pieces. (As mentioned before, writing tests involves trade-offs; writing extremely thorough tests may give you more confidence, but it takes longer to write the tests, and the tests become too brittle—too easily broken by insignificant changes.)

After updating this mailer test, let’s run the test again:
$ rails test test/mailers/notifier_mailer_test.rb
Run options: --seed 46405
# Running:
E
Error:
NotifierMailerTest#test_email_friend:
ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
    app/views/notifier_mailer/email_friend.html.erb:6
    app/mailers/notifier_mailer.rb:10:in `email_friend'
    test/mailers/notifier_mailer_test.rb:14:in `block (2 levels) in <class:NotifierMailerTest>'
    test/mailers/notifier_mailer_test.rb:13:in `block in <class:NotifierMailerTest>'
rails test test/mailers/notifier_mailer_test.rb:10
Finished in 0.279585s, 3.5767 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
Now we have a new error, which is a form of progress! This error may look familiar; back in Chapter 12, we had to configure this default_url_options setting to send emails in development. We need to also configure this setting for our test environment. Modify your config/environments/test.rb file to match Listing 16-19.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.
  # ... settings omitted for brevity ...
  config.action_mailer.perform_caching = false
  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test
  config.action_mailer.default_url_options = { host: 'http://example.com' }
  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr
  # Raises error for missing translations.
  # config.action_view.raise_on_missing_translations = true
end
Listing 16-19

Configuring Action Mailer Default URL Host in Test Environmenthttps://gist.github.com/nicedawg/b99964ceadc6f2bd4efef52e14caabbc

Now, running the NotifierMailer test again should show success! We still have a failing DraftArticlesMailer test, though. Let’s quickly fix that. Let’s modify our code in test/mailers/draft_articles_mailer_test.rb to match Listing 16-20.
require 'test_helper'
class DraftArticlesMailerTest < ActionMailer::TestCase
  test "no_author" do
    mail = DraftArticlesMailer.no_author('to@example.org')
    assert_equal "Your email could not be processed", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Please check your draft articles email address and try again.", mail.body.encoded
  end
end

These fixes were fairly minor; we provided the parameter which no_author expected and updated a couple of assertions to match reality. Now, try running the rails test command again. Success!

We only scratched the surface of Action Mailer tests, and we haven’t even talked about Action Cable tests, Active Job tests, or Action Mailbox tests. Entire books are devoted to testing the Rails framework; we won’t cover all types of testing here, but hopefully you feel more comfortable with testing Rails applications than you did before.

We’re not quite done with testing yet, though. Up to this point, we’ve covered unit test models and mailers, as well as writing functional controller tests which gave us the ability to quickly make some assertions about our controller’s behavior, but we realized those controller tests had some limitations. We’d like to have some tests—even if they’re slower—that approach testing the actual user experience.

System Testing

Rails defines one more type of test, and it’s the highest level of the bunch. System tests go much further than the controller tests we wrote earlier. Unlike controller tests, which basically just look for response codes, system tests can span multiple controllers and actions with full session support. System tests either are run by a Rack::Test driver, which is like a barebones simulated web browser, or can even be run by an actual browser installed on your workstation! They’re the closest you can get to simulating actual interaction with a web application. They test that the individual pieces of your application integrate well together.

It should be noted that Rails also supports another, similar type of test, called integration tests, though many developers feel there’s too much overlap between controller tests, integration tests, and system tests and opt for leaving integration tests out of their test suite. We won’t cover integration tests in this chapter, but check out https://guides.rubyonrails.org/testing.html#integration-testing if you’d like to learn more.

System Testing the Blog Application

Let’s get started by writing some system tests. In fact, we already have one; in an earlier chapter, when we used the generator to scaffold our articles controller, Rails created the test/system/articles_test.rb file for us. Let’s go ahead and try to run it:
$ rails test:system
Run options: --seed 27728
# Running:
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55167
... output omitted ...
[Screenshot]: /Users/brady/Sites/beginning-rails-6-blog/tmp/screenshots/failures_test_updating_a_Article.png
E
Error:
ArticlesTest#test_updating_a_Article:
StandardError: No fixture named 'one' found for fixture set 'articles'
    test/system/articles_test.rb:5:in `block in <class:ArticlesTest>'
... output omitted ...
Finished in 5.770051s, 0.6932 runs/s, 0.0000 assertions/s.
4 runs, 0 assertions, 0 failures, 4 errors, 0 skips

First of all, you may have been surprised to see that when running this system test, suddenly some Chrome browser windows opened and closed; don’t worry—your workstation hasn’t gone crazy! System tests in Rails are configured to use Chrome by default; those windows were actually launched by running the system test.

Looking at the test output, we see our tests failed, but for a familiar reason; we need to update the fixture name. We’ll fix that in a moment, but let’s keep looking at this test output.

First, we see “Capybara starting Puma…”; Capybara is a gem which Rails now includes and configures by default which gives your system tests the ability to control a real browser and to help make assertions. Puma is a Ruby server that can handle web requests and pass them to your Rails app. System tests start their own server so that the test environment is kept isolated from your development environment. It’s possible to change Capybara and Puma configuration in your test environment, but it’s nice to know it works out of the box.

Also, notice how when a test fails, a screenshot is generated; this can be very useful! Those browser windows flew by so fast, we wouldn’t have had a chance to see what was wrong; but the screenshot taken at the point of failure can sometimes help us debug our system tests.

Before we fix our fixture reference in our test, let’s make a quick change so that when we run our system tests, Chrome windows don’t take over our screen. Our system tests can continue to use Chrome in “headless” mode, which simply means in a way that isn’t visible on your workstation. Let’s switch to headless Chrome by modifying test/application_system_test_case.rb to match Listing 16-21.
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
end
Listing 16-21

Switching System Tests to Use Headless Chrome by Defaulthttps://gist.github.com/nicedawg/17d775d3bea48b5274e15f5bed9b41b5

As you can see, this ApplicationSystemTestCase class is a convenient place to change defaults for our system tests. It was using selenium—a library which knows how to control web browsers—to control Chrome, with a given screen size. By changing this to headless_chrome, our system tests will still run with the Chrome browser, but invisibly. It’s also possible to use other browsers, like Firefox and others; see https://github.com/teamcapybara/capybara#selenium for more information.

Run the system tests again with rails test:system; we should see the same failures as before, but without browser windows popping up during the test run. But now you know how easy it is to switch back to using visible Chrome windows if you want.

Okay, let’s get back to fixing our system test. First, let’s fix our immediate problem—the reference to our articles fixture. Modify the reference in the setup method of your test/system/articles_test.rb so it matches Listing 16-22.
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
  setup do
    @article = articles(:welcome_to_rails)
  end
  test "visiting the index" do
    visit articles_url
    assert_selector "h1", text: "Articles"
  end
  test "creating a Article" do
    visit articles_url
    click_on "New Article"
    fill_in "Body", with: @article.body
    fill_in "Excerpt", with: @article.excerpt
    fill_in "Location", with: @article.location
    fill_in "Published at", with: @article.published_at
    fill_in "Title", with: @article.title
    click_on "Create Article"
    assert_text "Article was successfully created"
    click_on "Back"
  end
  test "updating a Article" do
    visit articles_url
    click_on "Edit", match: :first
    fill_in "Body", with: @article.body
    fill_in "Excerpt", with: @article.excerpt
    fill_in "Location", with: @article.location
    fill_in "Published at", with: @article.published_at
    fill_in "Title", with: @article.title
    click_on "Update Article"
    assert_text "Article was successfully updated"
    click_on "Back"
  end
  test "destroying a Article" do
    visit articles_url
    page.accept_confirm do
      click_on "Destroy", match: :first
    end
    assert_text "Article was successfully destroyed"
  end
end
Listing 16-22

Fixing Articles Fixture Reference in test/system/articles_test.rbhttps://gist.github.com/nicedawg/a16e2ae42319be3641ca9d4bb57f0827

We fixed the reference to our articles fixture, but before running our system test again, let’s take a closer look at how this test works.

First, the overall structure looks familiar; just like the other tests we’ve written, there’s an optional setup method, and each test case is declared just like we’ve done before.

However, we see some new methods in our test cases; visit is a Capybara method which tells our browser to navigate to a particular URL. assert_selector is another Capybara method which lets us make assertions like “there is an H1 tag with the text ’Articles’.” click_on is another Capybara method which finds a button or link with the provided text (or even CSS selector) and tells the browser to click it. fill_in is yet another Capybara method that fills in an input field with provided data.

Capybara provides a vast array of methods (with options) to give you the ability to perform almost any browser function in your system tests; by default, Capybara only interacts with visible elements and will typically wait up to a couple of seconds for a given element to appear on the screen. There are too many methods and options to list here; visit https://github.com/teamcapybara/capybara#the-dsl for more information when you’re ready.

Running the system test again, we see new errors. (Remember, that’s progress!)
$ rails test:system
Run options: --seed 6356
# Running:
... output omitted ...
E
Error:
ArticlesTest#test_creating_a_Article:
Capybara::ElementNotFound: Unable to find link or button "New Article"
    test/system/articles_test.rb:15:in `block in <class:ArticlesTest>'
rails test test/system/articles_test.rb:13
Finished in 5.833557s, 0.6857 runs/s, 0.1714 assertions/s.
4 runs, 1 assertions, 0 failures, 3 errors, 0 skips

We see that our tests for creating, updating, and destroying an article failed, due to being unable to find links or buttons with the given tests. Similar to our controller test earlier in this chapter, we realize that only logged-in users are allowed to perform these actions, so we’ll need to add code to sign in a user for these tests.

Also, we remember that we only show the Edit and Destroy links when the logged-in author is hovering over the title, so we’ll need to add a hover command to those tests in order for those tests to pass. Also, this generated test wasn’t updated to reflect that we changed the article body to be handled by Action Text nor to reflect that the Published At field is a series of select boxes instead of a text input.

Let’s edit our test in test/system/articles_test.rb to match Listing 16-23 to address these problems. We’ll explain further after the listing.
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
  setup do
    @article = articles(:welcome_to_rails)
    @user = users(:eugene)
  end
  def sign_in(user)
    visit login_url
    fill_in "email", with: user.email
    fill_in "password", with: 'secret'
    click_button "Login"
  end
  def fill_in_rich_text(locator, content)
    find(locator).base.send_keys(content)
  end
  def set_datetime_select(locator, datetime)
    select datetime.strftime("%Y"),  from: "#{locator}_1i" # Year
    select datetime.strftime("%B"),  from: "#{locator}_2i" # Month
    select datetime.strftime("%-d"), from: "#{locator}_3i" # Day
    select datetime.strftime("%H"),  from: "#{locator}_4i" # Hour
    select datetime.strftime("%M"),  from: "#{locator}_5i" # Minutes
  end
  test "visiting the index" do
    visit articles_url
    assert_selector "h1", text: "Articles"
  end
  test "creating a Article" do
    sign_in(@user)
    visit articles_url
    click_on "New Article"
    fill_in_rich_text("#article_body", @article.body)
    fill_in "Excerpt", with: @article.excerpt
    fill_in "Location", with: @article.location
    set_datetime_select("article_published_at", @article.published_at)
    fill_in "Title", with: @article.title
    click_on "Create Article"
    assert_text "Article was successfully created"
  end
  test "updating a Article" do
    sign_in(@user)
    visit articles_url
    find(".article a", match: :first).hover
    find(".article .actions a", text: "Edit").click
    fill_in_rich_text("#article_body", @article.body)
    fill_in "Excerpt", with: @article.excerpt
    fill_in "Location", with: @article.location
    set_datetime_select("article_published_at", @article.published_at)
    fill_in "Title", with: @article.title
    click_on "Update Article"
    assert_text "Article was successfully updated"
  end
  test "destroying a Article" do
    sign_in(@user)
    visit articles_url
    find(".article a", match: :first).hover
    find(".article .actions a", text: "Delete").click
    assert_text "Article was successfully destroyed"
  end
end
Listing 16-23

Fixing Articles System Test in test/system/articles_test.rbhttps://gist.github.com/nicedawg/9e37f7000fd9761762c312bfcd92d24b

Whew! It took several changes, and some of them were complicated; we’ll explain the changes, but don’t worry if these changes don’t seem self-evident. It took this author several tries, Internet searches, and debugging using test output and screenshots to figure it out. System tests are often more tedious to implement and more fragile (since minor changes anywhere in the application can cause them to break), but the effort is worth it.

First, we loaded the eugene user in our setup method so we can log him in when necessary.

Next, we created a sign_in method which takes a user and performs the necessary Capybara operations to log them in. This method would be a great candidate for moving to our ApplicationSystemTestCase class so it can be shared between multiple system tests, but this is fine for now.

Next, we realized we couldn’t simply fill_in "Body", as test failures indicate that element didn’t exist (or wasn’t visible). Of course, we replaced the simple text area for an article’s body with the Action Text control in an earlier chapter. Unfortunately, there doesn’t seem to be an elegant way to populate the content for an Action Text rich text field, so we resort to some lower-level Capybara methods. We use find to grab a reference to a particular DOM element and then use base.send_keys to simulate typing into a specific element. Since this was a little tricky to figure out, we decided to make it a method called fill_in_rich_text ; again, this would be a great candidate for sharing and should probably be moved elsewhere, but it’s fine for now.

Next, we realized that fill_in "Published at" wouldn’t work, as our Published At field isn’t a simple text box, but rather a series of select boxes. Again, since this was tricky to figure out, we made it a separate method called set_datetime_select. Like our other custom methods, this is a great candidate for sharing between other system tests, but it’s fine here for now.

For our creating an Article test, we added a call to sign in the user, replaced the commands to populate the body and published_at fields with our custom commands, and removed the final click_on "Back" command because we had made changes to our app that rendered that final command invalid.

Our changes to the updating an Article test were similar, though we couldn’t just click_on "Edit"; we had to use some commands to find the first article link and hover over it and then click the Edit link within the article’s .actions DOM element.

Finally, our changes to the destroying an Article test were similar, except we had to remove the page.accept_confirm method, since our “Delete” link doesn’t trigger a confirmation dialog when clicked.

Running our system tests now, we should see sweet success:
$ rails test:system
Run options: --seed 29652
# Running:
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:50734
... output omitted ...
....
Finished in 6.055268s, 0.6606 runs/s, 0.6606 assertions/s.
4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Notice the runtime of our system tests; we only had four tests, but they took 6 seconds to run. Compared to our other tests—19 tests that took less than 1 second—system tests are quite a bit slower. It’s true that 6 seconds is not long at all, but imagine your application having hundreds of system tests; soon, the entire test suite may take 15, 20, 30, or even 60 minutes to run! As mentioned earlier, a challenge of writing automated tests for your application is deciding which types of tests to write for certain features of your application.

For fun, try running the tests again with visible Chrome by changing the reference to :headless_chrome in your test/application_system_test_case.rb file back to :chrome. If you watch closely, you’ll see the browser windows open and start navigating, filling out forms, and submitting forms by themselves!

Summary

This chapter served as an introduction to the Rails philosophy behind testing and stressed its importance as part of the development cycle. We toured some of the most common types of tests—unit tests for our models, controller tests, mailer tests, and system tests, but we only scratched the surface.

While it’s not feasible to cover everything there is to know about testing your Rails application in this chapter, hopefully you gained a good foundation and know where to look for more information.

Hopefully you’ve also understood how testing is an important part of the development cycle. Despite the fact that we left it until near the end of this book, it’s not something we should treat as an afterthought. Now that you know how to write a Rails application and how to test it, you can combine the steps: write some code, and then test it. As you get into the code/test rhythm (or better yet, test/code), you’ll find that you can write better, more reliable software. And you may sleep a little better at night too, knowing that your code changes have a safety net.

We should also mention that Rails’ default testing framework, minitest, is only a default choice. There are several other test frameworks available for the Ruby community. In fact, test frameworks become almost like a religion to developers. RSpec (https://rspec.info/) is a very popular choice, as is test-unit (https://test-unit.github.io/), which was actually the default Rails test framework before minitest. There is also Cucumber (https://cucumber.io/), which uses a language called Gherkin that lets you write tests in a more human-friendly manner.

No matter which framework you decide to use, make sure you test early and often. Not only does it ensure your application does what you expect but it is also frequently used as a source of documentation by developers moving into your project for the first time.

The next chapter will look at preparing your applications for a global audience through the use of Rails’ built-in internationalization and localization support.