In Progress
Unit 1, Lesson 1
In Progress

Naming Test Data

Video transcript & code

Today let's talk about testing. Specifically, let's talk about how we set up the data for a test.

Let's say we are testing a UserRepo class, whose job it is to run queries against a collection of users. Each user has a name, a user type, and timestamp for when they first became a user.

User = Struct.new(:name, :type, :since)

class UserRepo
  def initialize(user_list)
    @user_list = user_list
  end

  def eligible_for_treats(now = Time.now)
    @user_list
  end
end

We're writing a test for a user repo method that filters the user list for users who are eligible for certain special perks. This method should only return users who have been in the system more than 6 months, and who have a premium account. However, it should also include special "VIP" users regardless of when they signed up.

In order to test this, we set up four test users. The first has been in the system long enough to qualify, but only has a free account. We don't bother with a user name, since that's not relevant to this test.

The second is premium-level user, but only signed up a few days ago. The third is both premium and has been in the system for quite some time. The last is a VIP user.

We initialize a UserRepo object with this user list. Then we set up an assertion. We send the eligible_for_treats message and assert that the resulting list will only contain users 3 and 4.

require "./models"
require "rspec/autorun"

RSpec.describe UserRepo do
  it "identifies users eligible for special treats" do
    u1 = User.new(nil, :free, Time.new(2014, 1, 1))
    u2 = User.new(nil, :premium, Time.new(2014, 11, 15))
    u3 = User.new(nil, :premium, Time.new(2014, 1, 1))
    u4 = User.new(nil, :vip, Time.new(2014, 11, 15))

    repo = UserRepo.new([u1, u2, u3, u4])
    expect(repo.eligible_for_treats).
      to eq([u3, u4])
  end
end


# >> F
# >>
# >> Failures:
# >>
# >>   1) UserRepo identifies users eligible for special treats
# >>      Failure/Error: Unable to find matching line from backtrace
# >>
# >>        expected: [#<struct User name=nil, type=:premium, since=2014-01-01 00:00:00 -0500>, #<struct User name=nil, type=:vip, since=2014-11-15 00:00:00 -0500>]
# >>             got: [#<struct User name=nil, type=:free, since=2014-01-01 00:00:00 -0500>, #<struct User name=nil, type=:premium, since=2014-11-15 00:00:00 -0500>, #<struct User name=nil, type=:premium, since=2014-01-01 00:00:00 -0500>, #<struct User name=nil, type=:vip, since=2014-11-15 00:00:00 -0500>]
# >>
# >>        (compared using ==)
# >>
# >>        Diff:
# >>        @@ -1,3 +1,5 @@
# >>        -["#<struct User name=nil, type=:premium, since=2014-01-01 00:00:00 -0500>",
# >>        +["#<struct User name=nil, type=:free, since=2014-01-01 00:00:00 -0500>",
# >>        + "#<struct User name=nil, type=:premium, since=2014-11-15 00:00:00 -0500>",
# >>        + "#<struct User name=nil, type=:premium, since=2014-01-01 00:00:00 -0500>",
# >>          "#<struct User name=nil, type=:vip, since=2014-11-15 00:00:00 -0500>"]
# >>      # xmptmp-in4459ZnR.rb:12:in `block (2 levels) in <main>'
# >>
# >> Finished in 0.00128 seconds (files took 0.06969 seconds to load)
# >> 1 example, 1 failure
# >>
# >> Failed examples:
# >>
# >> rspec xmptmp-in4459ZnR.rb:5 # UserRepo identifies users eligible for special treats

This fails, because we haven't actually defined the method being tested to do anything other than return the entire user list unaltered. But we're not going to worry about making the test pass today. Instead, we're going to focus on the readability of the test failures.

RSpec has done its best to make this failure message meaningful, but it doesn't have a lot to work with. There's a ton of information here comparing the expected output to the actual output. Unfortunately, we have to look rally closely at it to make any sense of it.

A good test should communicate its intent well not just in the form of test code, but in the messages that result from a failure. According to this criterion, this test isn't very nice. Let's see if we can make it better.

First off, our data model gives us a way to identify the individual users in our set of test data, but we declined to make use of it because usernames weren't relevant to the functionality at hand. Let's revisit that decision and give these users some meaningful names.

We'll call the first user "Frankie Freeloader", because it represents a free account. We call the second, "Nelly Newbie" because it was recently created. We call the third "Larry Longtimer", because it represents an older account. And finally, we assign the moniker "Suzie Special" to the VIP account.

Let's run the test again.

require "./models"
require "rspec/autorun"

RSpec.describe UserRepo do
  it "identifies users eligible for special treats" do
    u1 = User.new("Frankie Freeloader", :free, Time.new(2014, 1, 1))
    u2 = User.new("Nelly Newbie", :premium, Time.new(2014, 11, 15))
    u3 = User.new("Larry Longtimer", :premium, Time.new(2014, 1, 1))
    u4 = User.new("Suzie Special", :vip, Time.new(2014, 11, 15))

    repo = UserRepo.new([u1, u2, u3, u4])
    expect(repo.eligible_for_treats(Time.new(2014, 11, 30))).
      to eq([u3, u4])
  end
end


# >> F
# >>
# >> Failures:
# >>
# >>   1) UserRepo identifies users eligible for special treats
# >>      Failure/Error: Unable to find matching line from backtrace
# >>
# >>        expected: [#<struct User name="Larry Longtimer", type=:premium, since=2014-01-01 00:00:00 -0500>, #<struct User name="Suzie Special", type=:vip, since=2014-11-15 00:00:00 -0500>]
# >>             got: [#<struct User name="Frankie Freeloader", type=:free, since=2014-01-01 00:00:00 -0500>, #<struct User name="Nelly Newbie", type=:premium, since=2014-11-15 00:00:00 -0500>, #<struct User name="Larry Longtimer", type=:premium, since=2014-01-01 00:00:00 -0500>, #<struct User name="Suzie Special", type=:vip, since=2014-11-15 00:00:00 -0500>]
# >>
# >>        (compared using ==)
# >>
# >>        Diff:
# >>        @@ -1,3 +1,5 @@
# >>        -["#<struct User name=\"Larry Longtimer\", type=:premium, since=2014-01-01 00:00:00 -0500>",
# >>        +["#<struct User name=\"Frankie Freeloader\", type=:free, since=2014-01-01 00:00:00 -0500>",
# >>        + "#<struct User name=\"Nelly Newbie\", type=:premium, since=2014-11-15 00:00:00 -0500>",
# >>        + "#<struct User name=\"Larry Longtimer\", type=:premium, since=2014-01-01 00:00:00 -0500>",
# >>          "#<struct User name=\"Suzie Special\", type=:vip, since=2014-11-15 00:00:00 -0500>"]
# >>      # xmptmp-in44592Mb.rb:12:in `block (2 levels) in <main>'
# >>
# >> Finished in 0.0012 seconds (files took 0.07377 seconds to load)
# >> 1 example, 1 failure
# >>
# >> Failed examples:
# >>
# >> rspec xmptmp-in44592Mb.rb:5 # UserRepo identifies users eligible for special treats

This output is slightly more usable. Instead of having to look closely at the individual fields of each user object, the user names give us a quick and obvious clue about what is notable about each object.

But this output is still needlessly busy and verbose for our needs. Let's return to the test once again.

This time, we chain on a call to map after sending the eligible_for_treats message. We supply the symbol name in place of a block, telling the map method to return a list of just the names of each object.

Then we change the expectation. Instead of a list of opaque numbered user variables, we supply a list of the user names we expect to be returned.

require "./models"
require "rspec/autorun"

RSpec.describe UserRepo do
  it "identifies users eligible for special treats" do
    u1 = User.new("Frankie Freeloader", :free, Time.new(2014, 1, 1))
    u2 = User.new("Nelly Newbie", :premium, Time.new(2014, 11, 15))
    u3 = User.new("Larry Longtimer", :premium, Time.new(2014, 1, 1))
    u4 = User.new("Suzie Special", :vip, Time.new(2014, 11, 15))

    repo = UserRepo.new([u1, u2, u3, u4])
    expect(repo.eligible_for_treats(Time.new(2014, 11, 30)).map(&:name)).
      to eq(["Larry Longtimer", "Suzie Special"])
  end
end


# >> F
# >>
# >> Failures:
# >>
# >>   1) UserRepo identifies users eligible for special treats
# >>      Failure/Error: Unable to find matching line from backtrace
# >>
# >>        expected: ["Larry Longtimer", "Suzie Special"]
# >>             got: ["Frankie Freeloader", "Nelly Newbie", "Larry Longtimer", "Suzie Special"]
# >>
# >>        (compared using ==)
# >>      # xmptmp-in44594Vd.rb:12:in `block (2 levels) in <main>'
# >>
# >> Finished in 0.00076 seconds (files took 0.06766 seconds to load)
# >> 1 example, 1 failure
# >>
# >> Failed examples:
# >>
# >> rspec xmptmp-in44594Vd.rb:5 # UserRepo identifies users eligible for special treats

When we run this test, the results tell a very clear story. We expected just Larry and Suzie to be in the results, but instead all four of our motley crew of users have appeared.

Now that we are using user names in our expectation instead of the user objects themselves, there's another change we can make. Before, we needed to assign each test user to a variable in order to later reference some of those variables in the assertion. But now we're only using the user variables to initialize the repo.

We modify the code to simply create a list of users and pass that to the repo initializer, without assigning any individual user variables.

We've now tightened up our test code, and gotten rid of some rather badly-named variables in the process. And by giving our test users mnemonic names, we've made diagnosing test failures much easier.

require "./models"
require "rspec/autorun"

RSpec.describe UserRepo do
  it "identifies users eligible for special treats" do
    repo = UserRepo.new([User.new("Frankie Freeloader", :free, Time.new(2014, 1, 1)),
                         User.new("Nelly Newbie", :premium, Time.new(2014, 11, 15)),
                         User.new("Larry Longtimer", :premium, Time.new(2014, 1, 1)),
                         User.new("Suzie Special", :vip, Time.new(2014, 11, 15))])
    expect(repo.eligible_for_treats(Time.new(2014, 11, 30)).map(&:name)).
      to eq(["Larry Longtimer", "Suzie Special"])
  end
end


# >> F
# >>
# >> Failures:
# >>
# >>   1) UserRepo identifies users eligible for special treats
# >>      Failure/Error: Unable to find matching line from backtrace
# >>
# >>        expected: ["Larry Longtimer", "Suzie Special"]
# >>             got: ["Frankie Freeloader", "Nelly Newbie", "Larry Longtimer", "Suzie Special"]
# >>
# >>        (compared using ==)
# >>      # xmptmp-in4459uOs.rb:10:in `block (2 levels) in <main>'
# >>
# >> Finished in 0.00077 seconds (files took 0.08855 seconds to load)
# >> 1 example, 1 failure
# >>
# >> Failed examples:
# >>
# >> rspec xmptmp-in4459uOs.rb:5 # UserRepo identifies users eligible for special treats

Keep this technique in mind next time you're writing a test that requires you to create exemplars of objects in various different states. See if there is a way to attach memorable names to the objects, and to compare the results based on those names rather than based on inspecting whole objects. If you can, you'll be able to make your tests more communicative both as code and in their failure messages.

Happy hacking!

Responses