Video

Want to see the full-length video right now for free?

Sign In with GitHub for Free Access

Notes

In prior steps we discovered some of the benefits of TDD:

  • suite of regression tests (easier changes)
  • positive design pressure
  • workflow benefit of tackling problem is small steps

But there's another benefit: tests are automated, living documentation!

Comments have a problem in that they may be outdated, which makes them worse than no comments at all if they say the wrong thing and inspire false confidence.

Four Phases of Testing

Tests can tell a story. These stories have four acts, which in test parlance are "phases":

  • Setup - get the conditions correct for the test
  • Exercise - perform the thing that you're testing
  • Verification - verify that the exercise did what you expected
  • Teardown - undo any conditions that shouldn't persist post-test

Test Phases in Action

Let's examine the test phases in an example.

describe "#promote_to_admin" do
  it "makes a regular membership an admin membership" do
    # setup
    membership = Membership.new(admin: false)

    # exercise
    membership.promote_to_admin

    # verify
    expect(membership).to be_admin
  end
end

(In this test the teardown phase isn't necessary.)

This test breaks its phases into distinct, easily identifiable parts. (You don't need the comments, those are here for emphasis.) It's often a good idea to add a line break between phases to help a developer (who might be you in the future!) to discern the borders of your tests' phases.

The test is also written to be explicit about what's being tested. A new Membership is not an admin membership by default, but Harry is setting admin: false in the setup phase to help communicate intent about what the test is doing.

Before, Let, and Mystery Guests

Often tests will obscure what's going on in the name of de-duplication. Here's an RSpec example that leverages a before block to execute some test setup and declares some memoized test variables using #let:

describe Membership do
  before(:each) do
    chocolate_membership.promote_to_admin
  end

  let(:user) { User.new("Bill Wonka") }
  let(:chocolate_group) { Group.new("Chocolate Factory") }
  let(:peach_group) { Group.new("Giant Peach Enthusiasts") }
  let(:chocolate_membership) { Membership.new(user: user, group: chocolate_group, admin: false) }
  let(:peach_membership) { Membership.new(user: user, group: peach_group, admin: false) }

  describe "#promote_to_admin" do
    it "makes a regular membership an admin membership" do
      expect(chocolate_membership).to be_admin
    end

    it "doesn't change other memberships" do
      expect(peach_membership).not_to be_admin
    end
  end
end

It can be tempting to employ these RSpec features to avoid duplication in test code, but they can make tests hard to follow and deprive the test of its ability to tell a clear story. let in particular can be problematic as the contents of the memoized variable won't be set until the variable is referenced at least once in the test. In the first example, peach_group and peach_membership doesn't exist yet, as it was never referenced during the test exercise.

DRY is a good principle to follow in code, but it's a means to an end, not the end itself. Often in tests, overly-aggressive de-duplication can lower the quality of test code, as it loses its explanatory capability.

Frequent use of before blocks and let may be a testing anti-pattern, but that doesn't mean you can't apply any DRY in your tests. Here's an example of a test setup that could be reused in other tests, but retains an explicit setup phase:

describe "#promote_to_admin" do
  it "makes a regular membership an admin membership" do
    membership_to_promote = build_non_admin_membership

    membership_to_promote.promote_to_admin

    expect(membership_to_promote).to be_admin
  end

  it "doesn't change other memberships" do
    membership_to_promote = build_non_admin_membership
    membership_to_not_promote = build_non_admin_membership

    membership_to_promote.promote_to_admin

    expect(membership_to_not_promote).not_to be_admin
  end

  def build_non_admin_membership
    user = User.new
    group = Group.new("A group")

    Membership.new(user: user, group: group, admin: false)
  end
end

Here basic Ruby methods rather than the RSpec DSL allow for clear declaration of intent along with pulling out complicated or repetitive setup out of the test example.

Additional Resources