Words and Code

One writer’s journey from words to code.

Tackling Those Tests, Part 1: The How, When, and What of Rspec Testing

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
group :development, :test do
  gem 'rspec-rails'
end

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
 rspec
No examples found.


Finished in 0.00023 seconds (files took 0.08619 seconds to load)
0 examples, 0 failures

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
class ReadingList < ActiveRecord::Base
  belongs_to :user
  has_many :books

  attr_accessor :read

  def books_read
    books.where(read: true).count
  end

  def books_unread
    books.where(read: false).count
  end

  def percentage_read
    read = books_read.to_f
    unread = books_unread.to_f

    calculate_percentage(read, unread)
  end

  def calculate_percentage(read, unread)
    ((read / unread) * 100).round(2)
  end
end

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 our reading_list_spec.rb, and stubbing out our tests with a block:
1
2
3
4
5
6
require 'rails_helper'

RSpec.describe ReadingList, :type => :model do
  describe "#percentage_read" do
  end
end

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
  describe "#percentage_read" do
    it "finds the number of read books in a list" do
    end

    it "finds the number of unread books in a list" do
    end

    it "calculates a percentage when given two values" do
    end

    it "calculates the percentage of books read in a list" do
    end
  end
  • 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 and it blocks:
1
2
3
  let(:list) { ReadingList.create!(title: "Vaidehi's List" }
  let(:book1) { list.books.build(title: "A Game Of Thrones", read: true) }
  let(:book2) { list.books.build(title: "A Storm Of Swords", read: false) }

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
RSpec.describe ReadingList, :type => :model do
  let(:list) { ReadingList.create!(title: "Vaidehi's List" }
  let(:book1) { list.books.build(title: "A Game Of Thrones", read: true) }
  let(:book2) { list.books.build(title: "A Storm Of Swords", read: false) }

  describe "#percentage_read" do
    it "finds the number of read books in a list" do
      expect(list.books_read).to eq(1)
    end

    it "finds the number of unread books in a list" do
      expect(list.books_unread).to eq(1)
    end

    it "calculates a percentage when given two values" do
      expect(list.calculate_percentage(3.0, 4.0)).to eq(75.00)
    end

    it "calculates the percentage of books read in a list" do
      expect(list.percentage_read).to eq(50.00)
    end
  end
end

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:

  1. Does the read attribute on a ReadingList object only accept a boolean value? What if someone tries to pass a non-boolean value as read?
  2. What is the default value of the read attribute?
  3. What if read is nil – what will break?
  4. What if the return value of books_unread is 0?

Just FYI, I discovered the answer to number 4, which looks like this:

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 an it 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 single it block. Check out more on the let and let! 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 use context 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.