Clean code for your Ruby tests

 Writing unit tests is an important part of software development. Tests verify assumptions and protect against future breakage. It’s important to give our tests the love and attention they deserve. If your tests are hard to read, other developers won’t understand what you are testing. They will have difficulty finding gaps in your tests during pull request code reviews. Changes to the test code will likely result in further problems. What is the difference between a good and bad test? Let's look at some examples to find out.

Look at this Calculator class.


class Calculator
  def add(a, b)
    a + b
  end

  def subtract(a, b)
    a - b
  end
end

The class has two methods, add and subtract. Now look an example of poorly written tests (I'm using Rspec).


RSpec.describe Calculator do
  it "returns added value" do
    c = Calculator.new
    expect(c.add(2,2)).to eq(4)
  end

  it "returns subtract value" do
    c = Calculator.new
    expect(c.subtract(2,2)).to eq(4)
  end
end

The Rspec file has two tests. The first test verifies the functionality of the add method. The second test verifies the functionality of the subtract method. There is a number of problems with these tests.

  1. Test descriptions are vague.
  2. Not using Rspec features to simplify tests.
  3. Unclear test expectations.
  4. Difficult to understand the method a test is verifying.

Now let's look at an example of well written tests. Please note, there are a number of additional tests we should write, but for the sake of simplicity we will leave them out.


RSpec.describe Calculator do
  context '#add' do
    it "returns sum of two values" do
      expected = 4
      actual = subject.add(2, 2)
      expect(actual).to eq(expected)
    end
  end

  context '#subtract' do
    it "returns difference of two values" do
      expected = 1
      actual = subject.subtract(3, 2)
      expect(actual).to eq(expected)
    end
  end
end

The test file has a number of changes which improve readability.

  1. The user of Rspec context group relevant tests. We also append '#' to the method name to imply the type of method, instance method. If the method were a class method, we would append '::' to the method name.
  2. Better descriptions for each tests.
  3. Explicitly declaring a variable for the expected and actual values.
  4. Instead of creating the Calculator class manual, we used the implicitly defined subject.

Additional tests for each method should be placed inside of their respective context. The improved structure and readability of the test code makes it easier for other developers to review and contribute. Better test coverage means less bugs, and that's always a good thing.

Additional Reading