Ruby on Rails is amply documented. Many Rails testing guides are comprehensive and curated tutorials that will walk you through a splendid test suit for your Rails 4 app from 2014. Things have changed since then! So, what are the best practices for testing Rails applications in 2021? Keep reading to learn more!
The Test Pyramid
Mike Cohn came up with The Test Pyramid in his book Succeeding with Agile, and Martin Fowler polishes the concept in his article The Practical Test Pyramid. In short words, The Test Pyramid is the notion that your test suite should be constructed in different layers:
- Write tests with different granularity
- The more high-level you get the fewer tests you should have
Indeed, Martin Fowler advises:
Stick to the pyramid shape to come up with a healthy, fast, and maintainable test suite: Write lots of small and fast unit tests. Write some more coarse-grained tests and very few high-level tests that test your application from end to end.
At the bottom are unit tests
In the first place, you should write unit tests for the public interface of your Models, Concerns, Services, etc.
When writing unit tests, watch out for some common mistakes:
- Avoid testing trivial code
- Avoid tautological tests
- Avoid tying your test-suite to your implementation
Test for observable behavior instead.
Again, Martin Fowler gives a great example:
"If I enter values x and y, will the result be z?"
"If I enter x and y, will the method call class A first, then call class B, and then return the result of class A plus the result of class B?"
In the middle of the pyramid are the Functional tests for your controllers
Here, test your controller actions and validations, and make sure they operate correctly on the database. The Rails 6 guides tells us:
"You should test for things such as:
- Was the web request successful?
- Was the user redirected to the right page?
- Was the user successfully authenticated?
- Was the appropriate message displayed to the user in the view?
- Was the correct information displayed in the response?"
In Rails 5, ActionController::TestCase was soft deprecated in favor of ActionDispatch::IntegrationTest. This means that Rails's controller tests are no longer supported. For compatibility, RSpec-Rails 4.0 still implements controller tests via the rails-controller-testing gem. For new Rails 6 applications, you should favor Integration Tests (Request Specs in RSpec).
- The official Testing Rails Applications guide for Rails 6 covers Functional Testing and Integration Testing. Both use ActionDispatch::IntegrationTest. The difference is a conceptual one: Functional testing should test a single controller, while Integration testing should cover workflows. In this guide, workflows will be tested with capybara in system tests.
- As RSpec-Rails 4.0 includes the deprecated ActionController::TestCase methods, you will have access to assigns() and assert_template, well, avoid using them. The Rails team got rid of them for a reason. Use assert_select instead.
The top of the pyramid: System Testing
System tests interact with your application via a browser, simulating what a user would do (clicking links and buttons, filling forms, etc.).
In a System Test, your aim should be to imitate a user flow. For example, on an e-commerce site, The User:
- Visits the home page and adds a product to the cart
- Clicks the checkout link and is required to create an account
- Creates an account
- Can add, edit or remove an address and set a payment method
- Returns to their cart, which still holds the product
- Can select a shipping method
- Can successfully complete the order (via a bogus payment method)
Also, an e-commerce site would probably create and test a similar flow for a user admin verifying stock, adding or removing variants, etc.
- RRSpec also includes Feature specs. They perform the same function as System specs, but they were implemented by the RSpec team before Rails had System specs. RSpec's System specs wrap Rails System specs and should be chosen for new applications.
- Delegate edge cases to Request specs.
Creating a pyramidal test suite is a great practice, however, what else should you keep in mind?
Test in phases
Testing in phases will make your test maintainable and readable as all of them will have the same flow:
- Setup the test data and collaborators
- Exercise the system under test (SUT)
- Verify that the result is as expected
In Rails you can remember these phases with the mnemonic "Arrange, Act, Assert. (The last phase is mostly handled automatically by Rails and RSpec: rolling back changes to the DB, resetting the test doubles, etc. )
Sticking to these phases will help you keep your suite readable and expressive.
Guidelines for using Mocks
Feel free to use mocks and stubs generously in Unit testing. As you work your way to the top of the pyramid try to involve the real collaborators for your SUT.
The key is to remain flexible in the use of mocks. As Martin Fowler mentioned in his article The Practical Test Pyramid:
"If it becomes awkward to use real collaborators I will use mocks and stubs generously. If I feel like involving the real collaborator gives me more confidence in a test, I'll only stub the outermost parts of my service."
Use verifying doubles in RSpec Mocks
For verifying doubles RSpec will check the stubbed methods are actually present on the underlying object (if available). This will make your test suite more reliable since you make sure the underlying objects are actually capable of responding as expected.
Test Double is the generic term for any kind of pretend object used in place of a real object for testing purposes. The name comes from the notion of a stunt double.
Finally, here is a handy cheatsheet for Rails and RSpec tests:
|RSpec Equivalent||From rails guides||From RSpec||Gotchas|
|System test||Feature specs
|System tests allow you to test user interactions with your application, running tests in either a real or a headless browser. System tests use Capybara under the hood.
The beauty of system testing is that it is similar to integration testing in that it tests the user's interaction with your controller, model, and view, but system testing is much more robust and actually tests your application as if a real user were using it. Going forward, you can test anything that the user themselves would do in your application such as commenting, deleting articles, publishing draft articles, etc.
|System specs are RSpec's wrapper around Rails' own
|Feature specs are conceptually the same as system specs implemented by RSpec before the Rails team created System tests.|
|Integration test||Request specs||Integration tests are used to test how various parts of your application interact. They are generally used to test important workflows within our application. Integration tests are a great place to experiment with all kinds of use cases for our applications.
AKA test a user flow
|Request specs provide a thin wrapper around Rails' integration tests and aredesigned to drive behavior through the full stack, including routing.||Functional tests inherit from ActionDispatch::IntegrationTest (The same as integration tests or RSpecs request specs)|
|Functional test||Request Specs||Functional test the various actions of a controller
Test views and routes as part of your controller specs
|Request specs provide a thin wrapper around Rails' integration tests and are
designed to drive behavior through the full stack, including routing.
A controller spec Inherits from ActionController::TestCase which was deprecated in Rails 5. Avoid using them.
|Functional tests inherit from ActionDispatch::IntegrationTest (The same as integration tests or RSpecs request specs)|