Every month I walk to the end of my driveway and stuff my mailbox with a tall stack of letters. Actually, I have to repeat this process a few times, because stack is too tall to fit in my mailbox all at once, and each month it gets a little taller. But I don't mind.
These letters are destined for Alaska, Australia, Belfast, Bucarest, Boston, Portland, Paris, Peru, Kansas and Kuala Lumpur. They also go to Denver and Dallas, Spain and Seattle, Memphis, Malaysia and Minneapolis. Others just make a short trip across town. The readers of my letters are scattered across every inhabitable continent, and sometimes I even cross the ocean to visit them. I've been doing this for years.
I'm the author of what I assume is one of the world's very few snail mail programming newsletters, Nonsense Monthly. I started it on a whim to promote my consulting services. Now it's become a whole business on its own. I print it myself, and I use Ruby to help.
How the sausage is made
When it's time to send each month's batch of letters, I run a command-line Ruby program which 1) grabs my paid subscribers from Stripe and then 2) combines that list with a list of courtesy recipients who receive the newsletter free of charge. (It's a form of advertising.) Finally, the program 3) generates a PDF of all my recipients' addresses which I then print onto label paper. That's where Ruby's involvement in my production process ends. The next steps for me are to slap stamps and labels onto envelopes, stuff those bad boys with letters and send them out into the world.
Separately, I have a command-line report which I run every so often which shows me my monthly expenses and profits. Its output looks like this.
Count Cost Profit
---------------------------------
US 80 $58.40
Int'l 26 $42.90
---------------------------------
Total 106 $101.30 $21.20
I call this table the Postage Summary Ledger. Using my test-driven-development LLM skill, Claude wrote me a series of tests for this feature. Here's a test for the Profit column of the ledger. (By the way, if you're doing anything that's actually important, don't use floats for monetary values like I am. Floats are liable to give you wrong answers. Using floats is only okay in my case because I'm just seeking a rough idea of my profits and nothing is actually at stake.)
Profit test: too much information
It's easy to see what this test expects the profit to be: $12.00. Can you tell how that profit is calculated, though?
describe "#monthly_profit" do
it "is monthly revenue minus total postage cost" do
postage_summary_ledger = PostageSummaryLedger.new(
us_recipient_count: 3,
us_monthly_cost: 5.00,
international_recipient_count: 2,
international_monthly_cost: 3.00,
monthly_revenue: 20.00
)
expect(postage_summary_ledger.monthly_profit).to eq(12.00)
end
end
I can't, at least not easily. Look at all the numbers!
3,
5.00,
2,
3.00,
20.00 and
12.00.
Which of these numbers are relevant to the test? Which of these values
need to be specifically what they are, and which are arbitrary? It's not
easy to tell.
Refactored test setup: just the essentials
Here's a refactored version of the test which hides the distracting details and only shows the information that's relevant to the test.
describe "#monthly_profit" do
it "is monthly revenue minus total postage cost" do
postage_summary_ledger = build_postage_summary_ledger(
monthly_revenue: 10.00,
us_monthly_cost: 2.00
)
expect(postage_summary_ledger.monthly_profit).to eq(8.00)
end
end
The only numbers in this version of the test are 10.00,
2.00 and 8.00. It doesn't take a math genius to
figure out that the 8 comes from subtracting 2 from 10. This test is
easier to understand than the original verson because it makes use of the
principle of abstraction: throwing away unneeded lower-level
details and emphasizing higher-level, essential information. Abstraction
is just as useful in test code as it is in application code.
Test helper definition
The abstraction in this test—hiding the irrelevant numbers—is
made possible by a helper method which initializes a
PostageSummaryLedger object with pre-populated values for
all the values that the object requires. Since the profit calculation
only needs monthly revenue and some expense (either US monthly
cost or international monthly cost, doesn't matter which one), it's okay
for all of the ledger's other attributes to be zero.
describe "#monthly_profit" do
it "is monthly revenue minus total postage cost" do
postage_summary_ledger = build_postage_summary_ledger(
monthly_revenue: 10.00,
us_monthly_cost: 2.00
)
expect(postage_summary_ledger.monthly_profit).to eq(8.00)
end
end
def build_postage_summary_ledger(**overrides)
defaults = {
us_recipient_count: 0,
us_monthly_cost: 0,
international_recipient_count: 0,
international_monthly_cost: 0,
monthly_revenue: 0
}
PostageSummaryLedger.new(**defaults.merge(overrides))
end
I defined the build_postage_summary_ledger helper method
inline, right at the bottom of the PostageSummaryLedger
test, because this file is the one and only place the helper is used, and
doing so helps my code maintain high
cohesion.
Why did I go to all the trouble to create a helper method when I could
have just given all of PostageSummaryLedger's attributes
default values of nil? Because the application code
doesn't ever need these attributes to have default nil
values, only the test code would need that, and it's not to my
taste to add useless and possibly misleading behavior to my application
code just to make the tests a little more convenient to write. The
application code should only serve one master: the needs of the
application.
Code that's easier to understand is easier to change. Code that's easier to change is cheaper to own. Most teams unfortunately don't give as much attention to the understandability of their test suites and they do to the understandability of their application code, but all of the same principles apply in both areas, and a general policy of investing in clear, understandable tests always pays off.