Regardless of where you fall on the endless discussion surrounding test-driven development, solid tests are absolutely essential for any Rails application beyond the most basic complexity. Quality tests allow you to refactor code, write new features, and upgrade libraries or the environment without accidentally breaking things or pushing bugs to production. Obviously, it’s much nicer to find out that you broke something because a test failed than because you got an angry email from a client or user. But for all the focus on testing in the Rails community, I’ve found surprisingly few resources teaching how to test from the ground up; I had to learn primarily from working with experienced developers on existing projects with test coverage. So I decided to write this blog series- targeted at developers who are comfortable in Rails but know little about writing tests, and aimed at getting you up to speed to the point where you can consume other resources about how to test better.
There are many, many opinions about how to best structure your tests. Since this is just a tutorial, I’m going to impose my own and then link to some further reading once you understand the basics of testing. I primarily use three kinds of tests for my applications, in three different scenarios:
- Feature tests aim to test a feature from end-to-end; that is, a feature test will pretend to be a user, log in to your application, click through whatever menus are necessary to reach the item being tested, perform some actions, and expect the page content to be correct. I find feature tests to be the most useful test because they’re the most comprehensive and also the most efficient. It’s much easier to write a test for every user “workflow” through your application than it is to write a test for every model and controller method; and if you have a bug at any level of your application, it will break one of your user flows and then you can diagnose the actual location of the problem.
- Model tests can effectively supplement feature tests. Generally speaking, when writing Rails you should aim to have “fat models and slim controllers”; that is, refactor as much logic as possible to a relevant model and then keep your controllers down to logic that invokes model methods before rendering a relevant view. This often means that your models end up performing fairly detailed logic; when this happens, targeted model tests can serve you well because they let you immediately know that the “building blocks” of your application are working correctly (or not).
- Controller tests are, in my opinion, the least useful kinds of tests except for one circumstance: APIs. Controller tests are targeted towards one particular controller method (like
show); they send an HTTP verb request to the controller (possibly with data) and then expect the controller to give the correct response. For regular web applications, feature tests subsume the need for most controller tests because if the controller is misbehaving, the user experience will be incorrect at some point. However, if you’re writing an API, controller tests are just the ticket- there isn’t really a “user workflow” to follow so feature tests are fairly inane; and it’s much more efficient to make sure each API method is behaving as it should under a variety of conditions.
In addition to those three assumptions about when to use particular tests, this series uses the following test stack, mostly because it’s what I’m most familiar with. After completing the series, you should hopefully be knowledgeable enough to try out other test utilities if you’re so inclined, but this stack includes all the basics that you need to write quality, feature-rich test:
- RSpec, usually stylized “rspec”- the default test framework for Ruby on Rails. rspec allows you to write tests that perform commands or actions and then expect certain results or responses, “failing” the test if the incorrect result occurs. For example, a test for a model method might expect the
Model#name method to concatenate and return the model’s
last_name attributes, and fail if the result is incorrect. Obviously, much more on this later.
- Capybara- allows you to simulate a user interacting with your application. Capybara will actually “open” your application in a browser, perform user actions (like click on the “Log in” link and log in), and expect certain page content to be in place (perhaps “You have logged in!” if the user presented correct credentials, or “Incorrect credentials” if the password was wrong). Capybara can use a number of browsers, but we’ll be using one called Poltergeist. Poltergeist is a headless browser- it renders the page HTML without actually displaying it in a browser window, which makes your tests a lot faster and cleaner because your computer doesn’t have to open a window and render a bunch of pages.
- capybara-screenshot- a Capybara addon gem that automatically generates a screenshot of the current page (as HTML, an image, or both) whenever a test fails. This is really useful because it provides extra information as to why your feature tests failed- you can see how your application actually looks after a series of user actions, and then figure out why it doesn’t look like it’s supposed to.
- FactoryGirl- a gem that allows you to create fake data to use in your tests. Using FactoryGirl, you set up a factory for each of your models, and instruct the factory how to generate appropriate test data (so, for example, you might generate a fake name and email for your User model). In tandem with FactoryGirl, we’ll rely on Faker- a gem that simply generates fake data for a variety of purposes (fake names, emails, addresses, paragraphs of text, etc.). We’ll also be using DatabaseCleaner- a gem that simply resets your test database between tests to ensure that data from one test doesn’t interfere with any others.
- simplecov- a gem that generates an HTML coverage report whenever you run your tests. By reading the report, you can see which lines of code aren’t being hit by any of your tests, which lets you know what features or methods still need to have tests written for them.
We’ll use a handful of other gems for specific test cases, but those are the basics. In the next post, I’ll cover setting up your test environment and writing your first test (with code samples for convenience).