Up until two weeks ago, I had one great fear: testing. And, to be clear, when I say “fear”, what I actually mean is sheer terror.
My test-writing anxiety stems from the fact that I’ve never really had to do it before. I mean, I’ve had to make tons of tests pass, which means that I read other people’s tests all the time. Yet I’ve somehow made it thus far in my coding career without ever having to write relatively complex tests of my own. But that all changed a few weeks ago, when I was forced to finally confront my fear of testing.
The thing about conquering fears, however, is that usually involves doing the very thing that you’re afraid of. So, I spent the better portion of a week learning how and when to write tests, all while encountering a couple painful bugs along the way. It was not a fun week, but the good news is that I can write a fully-functioning test suite now! And now that I know more about testing, I actually find it kind of fun – so fun, in fact, that I’m going to share it with you!
Ain’t No Spec Like Rspec
Before we get into the how and when of testing, we first need to setup our Rails application with rspec
, a behavior-driven development framework built specifically for testing in Ruby.
We’ll first want to add rspec-rails
to our the development and test group in our Gemfile
:
1 2 3 |
|
Next, we’ll run a quick bundle install
, and then generate a /spec
folder by running rails generate rspec:install
. We now have access to a rails_helper.rb
and spec_helper.rb
file inside of our /spec
directory.
Finally, we’ll want to add files for everything that we want to test. But let’s start simple for now and just test our ReadingList
model. The path to this spec file should be /spec/models/reading_list_spec.rb
, so we’ll need to add a models
directory and a reading_list_spec.rb
file.
Once we’ve done that, we can check that everything is setup properly by running our rspec
command:
1 2 3 4 5 6 |
|
You know what needs to happen next, right? It’s time for us to write some tests.
Okay, I feel your pain. But I promise, we’re going to get through this together.
Knowing What To Test
I’ve found that the best way to start writing tests is by picking one section to work on first. Otherwise, it can just be so overwhelming and might make you want to give up completely. Let’s take a look at what our ReadingList
model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Whoa, this seems like a lot. But fear not! Programming is nothing more than breaking down big problems into smaller, bite-sized pieces. And that’s exactly what we’ll do when writing these tests.
Let’s look at the percentage_read
method to start. This is the instance method that we’ll actually want to call somewhere in our view. It uses the number of books marked read
(which will always be a boolean true
or false
value), and calculates the User
’s reading progress on the list, returning a percentage.
But even though this is the method we want to test, a deeper look reveals that it actually relies and calls upon three other methods: books_read
, books_unread
, and calculate_percentage
. This should be a big red flag, because it means that we need to test these three methods individually, first. The flow of our code is actually directing us in our test-writing process: we can decide which tests to write and in which order by looking at our method’s dependencies.
So, let’s hop to it:
- We’ll start by first requiring
rails_helper
in ourreading_list_spec.rb
, and stubbing out our tests with a block:
1 2 3 4 5 6 |
|
We can use a describe
block to break up our tests into different sections. They will come in handy as our tests start to grow, and will make our test suite easier to read – not just when we come back to look at them later, but also when another developer digs through our code. The #
symbol before our method name denotes that percentage_read
is an instance method, another important distinction to make as we go about adding more tests.
- Next, we’ll describe what our method should do by using
it
blocks
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
- Now we need to add some data – but not too much! We only want enough data to test the functionality of our method. Let’s create a list with two books, one marked read and the other not marked read. We’ll add this before our
describe
andit
blocks:
1 2 3 |
|
Wait, what’s that let
doing in there? The answer is: something magical! It creates an instance of ReadingList
and makes a reference to it called list
, which is then accessible to us in each of our it
blocks. The let
syntax is an alternative to creating local variables inside every single one of our it
blocks.
- Finally, we’ll add some expectations for our model’s behavior when each method is called. Our finished test suite now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Each one of our tests is just a single line, specifying our exact expectations! Pretty awesome, right? This makes for relatively DRY code, which is pretty easy to understand.
Testing Your Assumptions
Now that we know how to write tests, it’s time to address the question of when and what to test. Here’s a good rule of thumb that I adhere to: test your assumptions. Whenever we write code, we make a ton of assumptions. The problem with making assumptions, however, is that you forget or don’t realize that you made them, and then they end up coming back to screw you.
In fact, even the tests we just wrote are based on a lot of assumptions. And there are a lot of things that we haven’t considered. For example:
- Does the
read
attribute on aReadingList
object only accept aboolean
value? What if someone tries to pass a non-boolean value asread
? - What is the default value of the
read
attribute? - What if
read
is nil – what will break? - What if the return value of
books_unread
is0
?
Just FYI, I discovered the answer to number 4, which looks like this:
Uh oh…I did a bad thing: pic.twitter.com/uFZEkyCPRl
— Vaidehi Joshi (@vaidehijoshi) April 3, 2015
This is all to say that we must write tests for behavior that should and should not occur. We’d probably want to write validations to prevent nil
values, and we’d definitely want to raise an error whenever we try to divide by 0
. We aren’t just testing for what we can see – we also need to test for things we can’t see, and any edge cases that we can think of.
Learning the how, when, and what of testing is a process that comes with time and practice. The more tests you write, the better you’ll get at testing. Of course, there are few tips and tricks of the testing trade that can very quickly and easily save you a lot of heartache.
Tune in again next Tuesday, when I’ll delve into generating fixtures for test data using FactoryGirl – a trick that’s going to make your testing life so much easier.
tl;dr?
- All
rspec
tests have anit
block, which describes what behavior is expected. This block should never be too big, and contains an assertion of what expected value should be returned. - The
let
syntax allows for lazy evaluation and keeps you from having to create a new instance of an object inside of every singleit
block. Check out more on thelet
andlet!
helper methods over on this Stack Overflow answer or on this blog post. - Use
describe
blocks to divide up your tests into sections, based on functionality and code cohesion. You can also usecontext
blocks to assert different scenarios that could occur during one method call. Read about the difference between describe and context. - Find out more about different rspec testing conventions at Better Specs.