Where to start with Rails testing
Getting started with Rails testing can be overwhelming because there’s so much to learn. I think the learning process can be made easier by following the following three steps.
Get familiar with Rails testing tooling
Build some practice projects
Get familiar with testing principles
In order to write tests in Rails it’s necessary to have some understanding of the tools that are used for Rails testing. But also, perhaps more than that, you need to have an understanding of the technology-agnostic principles that apply no matter what language or framework you’re using.
The principles of automated testing are quite numerous. Just as you could spend a lifetime studying programming and never run out of things to learn, you could probably spend a lifetime studying testing and never run out of things to learn.
So the trick early on is to find the few testing principles that you need in order to get started and ignore all the rest. You can learn other principles as you go.
Here are a few principles I think are helpful to be familiar with at the beginning.
I personally think TDD is life-changingly great. At the same time, I wouldn’t recommend being concerned with following TDD to the letter when you’re first getting started with testing.
The fundamental principle of TDD is the red-green-refactor loop. First you write a failing test for the behavior you want to bring into existence. Then you write the code to make it pass, only concerning yourself with making the test pass and not with code quality at all. Then, once the test is passing, go back to the code and refactor it to make it nice.
When I was getting started with testing I had many occasions where I wanted to write the test first but I wasn’t sure how. What should you do if that happens to you? My advice would be to give yourself permission to break the TDD “rules” and just write the code before the test. As you gain more experience with testing it will get easier and easier to write the test first.
Automated tests tend to consist of four steps: setup, exercise, verification and teardown. Here’s an example in MiniTest.
class UserTest < Minitest::Test def test_soft_delete_user user = User.create!(email: 'firstname.lastname@example.org') # setup user.update_attributes!(active: false) # exercise assert user.active == false # assertion user.destroy! # teardown end end
In the first step, setup, we create the data that the test needs in order to do its stuff.
In the second step, exercise, we walk the system under test* through the steps that are necessary to get the system into the state we’re interested in.
In the third step, assertion, we ask, “Did the code do the thing it was expected to do?”
Finally, in the teardown, we put the system back the way we found it. (In Rails testing an explicit teardown step is usually not necessary because tests are often run inside of database transactions. The data doesn’t have to get deleted because it never gets persisted in the first place.)
*System under test (SUT) is a fancy way of saying “the part of the application we’re testing”.
Test independence/deterministic tests
It’s important that when a test runs, we get the same result every time. The passing or failing of the test shouldn’t depend on things like the date when the test was run or whether a certain other test was run before it or not.
That’s why the teardown step above is important. If a test leaves behind data after it runs, that data has the potential to interfere with another test that runs after it. Again, in Rails, developers tend not to explicitly destroy each test’s data but rather make use of database transactions to avoid persisting each test’s data in the first place. The transaction starts at the beginning of each test and at the end of each test the transaction is aborted.
Fixtures and factories
The first step of a test, setup, can get quite tedious and repetitive. There are easier ways of bringing the test data into existence than by instantiating objects and creating database records at the beginning of every single test.
There are two ways that setting up test data is normally handled: factories and fixtures. Both strategies have pros and cons. Most projects I’ve worked on use either one or the other but there’s no reason they couldn’t be used together.
Fixtures in Rails are usually defined in terms of YML files. The fixture data is persisted once at the beginning of the test suite run. Each test still runs in a database transaction so that any data modifications the test makes will be undone before the next test is run.
Fixtures have the advantage of speed, although the trade-off is that the data setup is a little bit distant from the test code itself. It might not always be clear what the data is that a test depends on or where it’s coming from.
With factories, the data for each test is defined inside the test code itself instead of in a separate YML file. Factories have the advantage of clarity. If the tests were written well then it’s very clear what data the test depends on to run. The trade-off is speed.
Mocks and stubs
I bring up mocks and stubs to tell you that you can safely ignore them when you’re getting started.
You can get quite a ways with Rails testing before a lack of mock and stub knowledge will hinder you. But if you’re curious, the books xUnit Patterns and Growing Object-Oriented Software, Guided by Tests do a pretty good job of explaining the concepts.
Get familiar with Rails testing tooling
The two dominant testing frameworks for Rails are RSpec and MiniTest. RSpec seems to be much more popular although that certainly doesn’t mean it’s better.
“Should I use RSpec or MiniTest?” is a question I see lot. Luckily you can’t really choose wrong. The underlying testing principles are pretty much the same whether you use RSpec or MiniTest. The main difference between the two is that RSpec is built on a domain-specific language (DSL) while MiniTest is just Ruby. A lot of people find that RSpec’s DSL improves readability although the tradeoff is that you have to learn the DSL.
I personally started out with Test::Unit (very similar to MiniTest) when I was doing Rails projects on my own. Later I switched to RSpec because almost every job or freelance client I’ve had used RSpec. So even if you pick the “wrong” testing framework to start with, you can always switch to the other one later.
Since I’m an RSpec guy I’ll focus mainly on RSpec-related tools.
Factory Bot is the de-facto standard library for Rails. The main benefits of Factory Bot in my view are that it helps DRY up test setup code and that it eliminates the need to come up with arbitrary test data (e.g. fake names, addresses, etc.).
Factory Bot only goes so far with its ability to generate fake data. If I want to generate things like fake but valid email addresses, phone numbers, etc., I use Faker in conjunction with Factory Bot.
Build some practice projects
Just like with programming itself, the way to get good at testing is to just start doing it.
Here’s the way I generally go about developing a new feature or resource in Rails. (I do a Git commit after each step if not more often.)
rails generate scaffold <scaffold name> <options>
- Write some validation specs for the model I just created
- Write a feature spec for the “happy path” (i.e. all valid inputs) of the “create” and “update” functionality for the resource I just created
For the first few models I create in a Rails project I might not do much more than that. It’s usually not until a few models in that my application builds behavior that’s “interesting” enough to write a meaningful test for.
If you want to learn more about how I write Rails tests, I run free online Rails testing workshops about once a week. All the videos from previous sessions are up on YouTube (linked here). I also have a free Ruby testing micro-course.