Want to see the full-length video right now for free?
In prior steps we discovered some of the benefits of TDD:
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.
Tests can tell a story. These stories have four acts, which in test parlance are "phases":
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.
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.
[xunit]: http://xunitpatterns.com [lets-not]: https://robots.thoughtbot.com/lets-not