Write Better Tests With the Three-Phase Pattern

(This is a short excerpt from Practicing Rails. Sign up here to get the first chapter free!)

So, you’re working on a new app, and Rails just generated a test for you:

test/models/bug_test.rb
require 'test_helper'

class BugTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

You uncomment it, come up with a name, and you’re ready to write your test, right? But then what? What do you write first? What should your test code look like?

If you follow a simple pattern, you’ll turn those stubbed out lines of code into clear, well-structured test cases.

The three-phase test pattern

Your test cases should work in three phases:

  1. First, you set some stuff up (“Arrange”)
  2. Then, you do something (“Act”)
  3. Then, you make sure that what you expected to happen, actually happened. (“Assert”)

For instance, imagine you were testing a method on an array in Ruby. Following this pattern, your test could look like:

test "Array#sort will sort an array of numbers" do
  # arrange
  unsorted_array = [7, 4, 2, 3]
  
  # act
  sorted_array = unsorted_array.sort

  # assert
  assert_equal [2, 3, 4, 7], sorted_array
end

Simple enough. But every part of the test has a place to go, and each stage of the test almost tells you how to write it.

Sometimes, you won’t need an Arrange phase, or the Act and Assert phases will be combined. But it still helps to think about all three phases as you write your tests.

The Assert phase gotcha

There’s a trick to the Assert phase: you shouldn’t use the same logic that the Act phase used in the Assert phase. You should always take two paths to the same answer. Otherwise, you won’t notice bugs in the code you’re calling, because it’s just getting called again in the Assert phase.

For example, if you’re doing some math:

test "average returns the average of a set of numbers" do
  # arrange
  numbers = [1, 2, 3, 4]
  
  # act
  average = numbers.average

  # assert
  
  # this is bad
  assert_equal [1, 2, 3, 4].average, average

  # this is better
  assert_equal 2.5, average
end

Calling [1, 2, 3, 4].average again in the Assert phase is bad, because average could return almost anything and that assertion would still pass.

Here, that’s pretty clear. But even when things get more complicated, make sure you’re not just running the same code twice. Otherwise you’re only verifying that your method was called, not that it worked the way you expect it to.

Usually, the easiest way to take a second path to the answer is to find the answer by hand and hardcode it. It can be brittle, but it’s better than your tests breaking without you realizing it.

Why three phases?

If you split your tests into those three phases, you have simpler questions to answer. Instead of “How should I write this test?”, you can focus on each phase: “How should I set this test up?”, “What am I testing?”, “What should the answer look like?”

These questions still might not have easy answers, but the answers will be a lot easier than thinking about the entire test at once. And if you’re lucky, you can even share phases between related tests, making your next test much less painful to write.

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments