chapter two

2 Testing

 

2.1 Fundamentals

2.1.1 Avoiding 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 prices:

ch02 figure 1

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:

ch02 figure 2

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:

ch02 figure 3

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:

ch02 figure 4
=== 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:

2.1.2 Code Coverage

2.1.3 Line vs. Branch Coverage

2.1.4 Property-Based Testing

2.2 Unit Tests

2.2.1 Common Arguments Against Unit Tests

2.2.2 10 Unit Tests Properties

2.2.3 Unit Tests As Documentation

2.2.4 Test Behavior, Not Implementation

2.2.5 Test-Driven Development (TDD)