This is part three of an ongoing series on getting started and comfortable with testing Rails applications. I appreciate your feedback along the way.
We’ve got all the tools we need for building a solid, reliable test suite—now it’s time to put them to work. We’ll get started with the app’s core building blocks—its models.
In this post, we’ll complete the following tasks:
We’ll create our first spec files and factories for existing models by hand (though the handy RSpec generators we configured in Part 2 can be used as templates when adding future models to our application).
I think it’s easiest to learn testing at the model level because doing so allows you to examine and test the core building blocks of an application. (An object-oriented application without objects isn’t very useful, after all.) Well-tested code at this level is key—a solid foundation is the first step toward a reliable overall code base.
To get started, a model spec should include tests for the following:
This is a good time to look at the basic structure of an RSpec model spec. I find it helpful to think of them as individual outlines. For example, let’s look at our main
Contact model’s requirements:
We’ll expand this outline in a few minutes, but this gives us quite a bit for starters. It’s a simple spec for an admittedly simple model, but points to our first three best practices:
it) only expects on thing. Notice that I’m testing the
lastnamevalidations separately. This way, if an example fails, I know it’s because of that specific validation, and don’t have to dig through RSpec’s output for clues.
itis technically optional in RSpec; however, omitting it makes your specs more difficult to read.
With these best practices in mind, let’s build a spec for the
Open up the
spec directory and, if necessary, create a subdirectory named
models. Inside the subdirectory create a file named
contact_spec.rb and add the following:
We’ll fill in the details in a moment, but if you’d like you can now run the specs from your command line:
You should see output similar to the following:
Great! Four pending specs—let’s make them pass.
As we add additional models to the contacts manager, assuming we use Rails’
model generator to do so, this file (along with an associated factory) will be added automatically. (If it doesn’t go back and configure your application’s generators now.)
I won’t spend a lot of time bad-mouthing fixtures—frankly, it’s already been done. Long story short, there are two issues presented by fixtures I’d like to avoid: First, fixture data can be brittle and easily broken (meaning you spend about as much time maintaining your test data as you do your tests and actual code); and second, Rails bypasses Active Record when it loads fixture data into your test database. What does that mean? It means that important things like your models’ validations are ignored. This is bad!
Enter factories: Simple, flexible, building blocks for test data. If I had to point to a single component that helped me see the light toward testing more than anything else, it would be Factory Girl, an easy-to-use and easy-to-rely-on gem for creating test data without the brittleness of fixtures. Since we’ve got Factory Girl installed courtesy of the
factory_girl_rails gem we installed earlier, we’ve got full access to factories in our app. Let’s put them to work!
Back in the
spec directory, add another subdirectory named
factories; within it, add the file
contacts.rb with the following content:
This chunk of code gives us a factory we can use throughout our specs. Essentially, whenever we create test data via
Factory(:contact), that contact’s name will be John Doe. This is probably adequate for our first round of model specs, but I like to provide my specs with more random data. Enter the Faker gem. Edit
contacts.rb to include it:
Now our specs will use random, sometimes humorous names for each generated contact. Notice that I pass Faker’s
first_name method inside a block—Factory Girl considers these “lazy attributes” as opposed to the statically-added strings our initial factory had.
Return to the
contact_spec.rb file we set up a few minutes ago and locate the first example (
it "has a valid factory"). We’re going to write our first spec—essentially testing the factory we just created. Edit the example to look like the following:
This single-line spec uses RSpec’s
be_valid matcher verify that our new factory does indeed return a valid contact.
Run RSpec from the command line again and you should see one passing example, with three pending.
Validations are a good way to break into automated testing. These tests can usually be written in just a line or two of code, especially when we leverage the convenience of factories. Let’s add some detail to our
firstname validation spec:
Note what we’re doing with Factory Girl here: First, instead of the
Factory.create() approach, we’re using
Factory.build(). Can you guess the difference?
Factory() builds the model and saves it, while
Factory.build() instantiates a new model, but doesn’t save it. If we used
Factory() in this example it would break before we could even run the test, due to the validation.
Second, we use the
Contact factory’s defaults for every attribute except
:firstname, and for that we pass
nil to give it no value. In other words, instead of the default name of John Doe our
Contact factory would normally give us, it returns John. This is an incredibly convenient feature, especially when testing at the model level. You’ll use it a lot in your tests—starting with models, but more in other tests, too.
Run RSpec again; we should be up to two passing specs with two pending. We can use the same approach to test the
You may be thinking that these tests are relatively pointless—how hard is it to make sure validations are included in a model? The truth is, they can be easier to omit than you might imagine. If you think about what validations your model should have while writing tests (ideally, in a Test-Driven Development pattern), you are more likely to remember to include them.
In addition, testing validations becomes more important when they are more complex than simply validating presence or uniqueness. For example, let’s say we want to make sure we don’t duplicate a phone number for a user—their home, office, and mobile phones should all be unique to them. How might you test that?
Phone model spec, you might have the following example:
And make it pass with this validation in your
That’s not a typo in the previous sample spec—
Factory() is a shortcut for
Of course, validations can be more complicated than just requiring a specific scope. Yours might involve a complex regular expression or a custom method. Get in the habit of testing these validations—not just the happy paths where everything is valid, but also error conditions.
It would be convenient to only have to refer to
@contact.name to render our contacts’ full names instead of creating the string every time; let’s implement that feature in our
Contact class now:
We can use the same basic techniques we used for our validation examples to create a passing example of this feature:
Let’s test the
Contact model’s ability to return a list of contacts whose names begin with a given letter. For example, if I click S then I should get Smith, Sumner, and so on, but not Jones. There are a number of ways I could implement this—for demonstration purposes I’ll show one.
First, let’s say we choose to add this functionality via a class method like the following:
To test this, let’s add the following to our
We’ve tested the happy path—a user selects a name for which we can return results—but what about occasions when a selected letter returns no results? We’d better test that, too. The following spec should do it:
This spec uses RSpec’s
include matcher to determine if the array returned by
Contact.by_letter("J")—and it passes! We’re testing not just for ideal results—the user selects a letter with results—but also for letters with no results. However, a problem is brewing in our spec—can you spot it?
Our spec currently has some redundancy: We create the same three objects in each example. Just as in your application code, the DRY principle applies to your tests (with some exceptions; see below). Let’s use a few RSpec tricks to clean things up.
The first thing I’m going to do is create a
describe block within my
describe Contact block to focus on the filter feature. The general outline will look like this:
Let’s break things down further by including a couple of
context blocks—one for matching letters, one for non-matching:
context are technically interchangeable, I prefer to use them like this—specifically,
describe outlines a function of my class;
context outlines a specific state. In my case, I have a state of a letter with matching results selected, and a state with a non-matching letter selected.
As you may be able to spot, we’re creating an outline of examples here to help us sort similar examples together. This makes for a more readable spec. First, let’s finish cleaning up our reorganized spec with the help of a
before hooks are vital to cleaning up unneeded redundancy from your specs. As you might guess, the code contained within the
before block is run before each example within the
describe block. Since I’ve indicated that the block should be run before each example, RSpec will create them for each example individually. In this example, my
before block will only be called within the
describe "filter last name by letter" block—in other words, my original validation specs will not have access to
Speaking of my three mock contacts, note that since they are no longer being created within each example, I have to assign them to instance variables, so they’re accessible outside of the
If your spec requires some sort of post-example teardown—disconnecting from an external service, say—you can also use an
after block to clean up after your examples. Since RSpec handles cleaning up the database for me, I rarely use
before, though, is indispensable.
Okay, let’s see that full, organized spec:
Run the spec—if you’ve configured RSpec to use documentation format you should see a nice outline like this:
We’ve spent a lot of time in this chapter organizing specs into easy-to-follow blocks. Like I said,
before blocks are key to making this happen—but they’re also easy to abuse.
When setting up test conditions for your example, I think it’s okay to bend the DRY principle in the interest of readability. If you find yourself scrolling up and down a large spec file in order to see what it is you’re testing, consider duplicating your test data setup within smaller
describe blocks—or even within examples themselves.
That said, well-named variables can go a long way as well—for example, in the spec above we used
@johnson as test contacts. These are much easier to follow than
@user2 would have been.
And that’s how I test models, but we’ve covered a lot of other important techniques you’ll want to use in other types of specs moving forward:
contextto sort similar examples into an outline format, and
afterblocks to remove duplication. However, in the case of tests readability trumps DRY—if you find yourself having to scroll up and down your spec too much, it’s okay to repeat yourself a bit.
With a solid collection of model specs incorporated into your app, you’re well on your way to more trustworthy code. Next time we’ll apply and expand upon the techniques covered here to application controllers.
I stand with the Black community against systemic racism, police violence and brutality, intolerance, and hate in the United States and worldwide. We must all demand better from our leaders, and ourselves. Stop tolerating intolerance.
While you're here, please consider making a donation to Black Girls CODE, who do great, important work to provide opportunity to underprivileged girls interested in tech, or any organization working toward equity and safety for all, not just the privileged. Thank you.
If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.
Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.