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.
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
- 1.
Test directories for controller, model, mailer, helper, system, and integration tests (and more)
- 2.
Fixtures for easily working with database data
- 3.
An environment explicitly created for testing
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.
Users Fixtures in test/fixtures/users.yml
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
Generated Article Unit Test in test/models/article_test.rb
- 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.
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.
Within a test case, assertions are used to test expectations. The “Testing with Assertions” section later in this chapter explains how these work.
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
- 1.
Creating a new article
- 2.
Finding an article
- 3.
Updating an article
- 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
Articles Fixtures in test/fixtures/articles.yml: https://gist.github.com/nicedawg/27d1df26c994bbd0c7579498cd8d91e7
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!
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
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.
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).
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.
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
Test Case for Finding an Article in test/models/article_test.rb: https://gist.github.com/nicedawg/71fbefeec56ce2ada47e956d3115b243
Sure enough, finding works! So far, so good.
Adding an Update Test
Test Case for Updating an Article in test/models/article_test.rb: https://gist.github.com/nicedawg/24dc226151646294245b7c1f2380dd0b
Adding a Destroy Test
Test Case for Destroying an Article in test/models/article_test.rb: https://gist.github.com/nicedawg/813a3e5f9bf5d3fdbfe888a2dc4e3360
We’ve done it! We’ve successfully tested the happy path for each CRUD operation for our Article model.
Testing Validations
Test Case for Validations in test/models/article_test.rb: https://gist.github.com/nicedawg/603e64d1846086488873757624745245
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
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.
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.
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
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.
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.
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. |
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.
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.
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!
Adding login_as to Test Cases in ArticlesControllerTest: https://gist.github.com/nicedawg/ec58c6d0b771078a308580fbc2c1743c
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.
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!
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.
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?
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.)
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.
Fixing NotifierMailerTesthttps://gist.github.com/nicedawg/c467c97ddfce1dbfd99bf6641ed2e06f
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.)
Configuring Action Mailer Default URL Host in Test Environmenthttps://gist.github.com/nicedawg/b99964ceadc6f2bd4efef52e14caabbc
Fixing DraftArticlesMailerTesthttps://gist.github.com/nicedawg/f03b344103e4a4f66370fe668b8be576
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
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.
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.
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.
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.
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.
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.