I started learning Ruby on Rails over a year ago, and as most beginners, I chose the popular Ruby on Rails Tutorial as my initial guide. Because there was so much new material to absorb, I decided to skip the sections about testing (and I’m glad I did or my head would’ve exploded). When I finished the book, I decided to build a Rails app called Phindee in order to solidify what I had just learned. I never went back to learn about testing, however. Now over a year later, I did just that and was finally able to write a solid test suite for the app.
To be honest, I was a bit reluctant to pickup testing at first. I knew it was important to test code (and I did that by sprinkling
Let me share it with you.
Test-driven development (TDD) is an approach to software development in which we first write a test for a desired functionality, then run the test to make sure it fails, and only then do we implement the said functionality. Once implemented, we run the test once more to make sure our implementation behaves the way our test says it should.
We write and run a failing test first for two reasons:
- it helps guide our implementation due to the fact that we’ve already identified what the result should look like, and
- it makes sure that the test is actually covering the functionality we think it is, because it’s easy to write a test that doesn’t really check what we think its checking
The benefits of TDD are many, but the way I see it, it boils down to three main ones: peace of mind, saved time, and better code.
Peace of Mind
How many times have you found yourself wanting to refactor an ugly mess of code, but due to the fear of breaking things, you ended up ditching the effort all together? This happens to me all the time, and I hate it. It doesn’t need be this way though. Since adding test cases to Phindee, I’ve refactored more than half of my helper functions without any worry of breaking things.
But it gets better. Testing not only allows you to refactor with confidence, you also get to deploy with confidence, and this comes as part of the package, without any additional effort.
This kind of peace of mind is possible because a test suite catches bugs in your code like no other. You don’t even need to write a large number of tests to reap the rewards; a few well-written test cases can go a long way.
Let me ask you this: Would you rather run a command that looks for bugs in your code on demand, and tells you exactly where to look if it finds them, or would you rather have your users discover the bugs in production, thereby sending you on a frantic bug-hunting spree? It’s a no brainer, yet all too often we find ourselves discovering bugs in production when they could’ve easily been discovered in development.
The beauty with having a test suite is you write your tests once, and running them on demand is as simple as typing a short command. The amount of time this saves is enormous. Of course, I’m not saying that writing test cases means you’re production environment will be bug free because software is never bug free; but it will help you track down most bugs before they reach production and do so in a fraction of the time it would’ve taken otherwise.
Now that Phindee is backed up by a solid test suite, my code has drastically improved in quality because I was finally able to refactor it. It’s simpler, and there is now less of it.
Furthermore, having to write test cases for individual methods has also forced me to write simpler, decoupled methods. You see, it’s hard to write test cases for methods that do more than one thing and happen to be entangled with one another. And this is the reason why test cases lead to cleaner, simpler code. As a result, tracking down bugs is even easier, which means more saved time.
How It’s Done
Now that we’ve covered the benefits, I’d like to show you how easy it is to do the testing. Note that I will be using the testing library called Test Unit that ships by default with Rails, instead of the RSpec framework used by the Ruby on Rails Tutorial. (I’ll discuss why a bit later.)
Rails provides directories for five different categories of tests by default: helper tests, unit tests (directory is called
models), functional tests (directory is called
controllers), mailer tests, and integration tests. But before I go into them, I first need to introduce fixtures.
Fixtures are defined in YAML files, and their role is to initialize our models with sample data for the sole purpose of testing. They allow us to then easily use this data in our test cases without corrupting our development database. As an example, below is a fixture file for a model in Phindee called
1 2 3 4 5 6 7
Here I created two instances of the
Place model (
grill) and initialized their
website attributes. The data is now ready to be used in our test cases. Because YAML is beyond the scope of this post, I won’t go into any more detail, but I encourage you to learn more.
Now that we know about fixtures, we’re ready to learn about the different types of tests we can write for a Rails app. To better explain each type, I will show examples from Phindee.
Helper tests are just what they sound like—they’re tests for your helper methods. When you create a controller using the
rails generate controller NAME command, Rails automatically creates a
NAME_helper_test.rb file inside
test/helpers to write the tests in. Below is what one of my helper tests for Phindee looks like:
1 2 3 4 5 6 7 8 9
assert_equal method makes sure that
humanize_days(‘2’) returns a string with a value of
’monday’; if it doesn’t, it will raise an error. Because the
humanize_days method understands three different string formats, I test each one once. If one of the three calls fails, it will tell me exactly which one failed, thereby making debugging easier. All it takes is three lines of code, and my method is fully tested.
In practice, we would typically first write these tests, run them to make sure they’re failing, and only then would we start their implementation.
Unit tests are there to test your models. The
rails generate model NAME command creates a file for these tests called
NAME_test.rb inside the
test/models directory. Below are two tests from Phindee for an attribute called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
setup method is not an actual test case; it’s just a method that gets called before each test case is executed. It simply initializes an instance variable called
@place with the fixture we defined earlier called
thai. This makes the
@place instance variable available inside each subsequent test case.
The first test case sets the
name attribute to
nil and calls the
assert method to check that the
valid? method returned
false. In other words, it’s checking for the line below:
The second test makes sure that a
name attribute that exceeds the maximum length of 50 characters is not valid. This means it will look for a
length helper with a
maximum value set to 50, like so:
And finally, the third test makes sure that duplicates are not valid, which means it’ll look for a
uniqueness helper set to
You may be wondering what’s the point of all this? Well, if you ever accidentally delete a uniqueness declaration, for example, the test suite will let you know, and you will be able to fix it before you push your code to production and wreak havoc in your database.
Functional tests are there to test your controllers, although you can also use them to test your views and verify that important HTML elements are present. Running
rails generate controller NAME creates a file for these tests called
test/controllers. Let’s look at an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
assert_not_nil method makes sure the variable that the
assigns method retrieves is actually initialized. Note that
:geojson are instance variables inside the controller, but here they’re symbols.
All the other remaining assertions use the
assert_select method to select an HTML element using the familiar CSS syntax and make sure it’s value is what we expect it to be. As you can see, the method is quite powerful; it can check for a specific string, evaluate a regular expression, and check for a certain number of elements using the
count method, among other things.
I’m only scratching the surface here of what’s possible with functional tests, and I encourage you to check out the official Rails guide on testing to learn more.
As you might guess, mailer tests are there to test mailer classes. A
NAME_mailer_test.rb file is created inside
test/mailers anytime you run
rails generate mailer NAME. You can test your mailers in two different ways:
- test the mailer in isolation to make sure its output is what you expect (using unit tests)
- test the controllers and models that use the mailers to make sure the right email is sent at the right time (using functional tests)
When testing your mailers with unit tests, you’ll use fixtures to provide sample data demonstrating how the output should look. I don’t have any examples of mailer tests to show because I have not yet needed to implement email functionality for Phindee, but the Rails guide should give you a good feel for what they look like.
Last but not least, we have integration tests, which are used to test controllers interacting with one another; they’re the “big picture” tests that make sure important workflows within your application are as bug free as possible. I haven’t written any integration tests for Phindee either because the app is simple enough that I only need one controller currently, but that will change in the near future, and I will update this section accordingly; in the meantime, feel free to see the Rails guide for examples.
One final thing I’d like to mention is the
test/test_helper.rb file, which holds the default configuration for our tests. This file is included in all the tests, which means any methods added here are automatically available in all our tests. Pretty neat.
Why Not RSpec?
I chose not to use RSpec because I wanted learn about the way testing is done in Rails by default and see how it compares with RSpec. So far, it seems like both approaches are equally capable of doing everything necessary to sufficiently test your code; they just take a different approach with regards to the way you write the tests. RSpec’s syntax seems more verbose and reads like English, while Test Unit’s syntax is more terse.
Currently, I’m leaning towards Test Unit because its terse syntax means less typing, and since it comes baked in with Rails, there is no need to inflate the code base with additional gems. (Rails 4 actually incorporated a library called MiniTest into Test Unit, which now offers support for RSpec-like syntax.)
But all this is irrelevant because what truly matters is that you practice test-driven development. Hopefully, I’ve shown you how easy it is to do it and convinced you that the benefits of doing so more than make up for the effort of writing them.