Hello! Today, let’s discuss the problem of adding logic in tests.
Consider the following code, which calculates the total savings (or economy) we can realize by applying a discount percentage to a list of prices1:
Now, we want to write a unit test to validate this economy
function. We might want to reuse the same logic as the function itself to compute the expected result in the test like this:
The test passes ✅, and everything looks good—until we deploy to production and realize there’s a bug! Can you guess what’s wrong in both the function and why the test didn’t catch it?
If we print the results of the tests with the three prices and a 10% discount, we get -17.5
. Yet, as it’s an economy value, we expected a positive value from this function. The issue lies in the negative sign of our economy
function:
Instead of calculating a positive discount, the logic applies a negative value.
Why didn’t we catch this bug in our tests? To calculate the expected economy, we duplicated the same flawed logic in the test itself, thus reproducing the bug in the test.
Let’s rewrite the test with a hardcoded expected value. In this case, it would become apparent that something was wrong in our code:
=== RUN TestEconomy
wrong economy: want 17.5, got -17.5
This example highlights an important principle in testing: avoid adding logic to tests, especially logic that mirrors the function under test.
A test should:
Use hardcoded values for expected results whenever possible.
Favor simplicity and readability, even if it means some repetition.
Be written in a way that prevents the reader from doing mental computation to verify the test’s correctness.
A test shouldn’t:
Mirror the code logic it is meant to validate.
Include elements like loops, concatenations, or complex calculations that could obscure the intended result.
Tomorrow, we will discuss code coverage.
The examples are in Go.
The underlying principle is double checking. If you get to the same answer through two methods, you’re much more confident.