#51: Test Behavior, Not Implementation
Hello, and welcome back to The Coder Cafe! This week, we will explore unit tests and start with an essential principle in unit testing: testing behavior, not implementation.
After writing a function, it might seem natural to add a test to verify whether it works correctly. However, it’s important to realize that we shouldn’t always have a one-to-one relationship between functions and unit tests. Instead, our tests should focus on how the code behaves as a whole, not just on individual methods.
Consider writing an in-memory cache to store the balance for each customer ID. Our class contains a hashmap that stores the data and exposes methods to get and set the customer balance1:
class CustomerBalance {
private map<string, float> cache;
public get(id string) float { ... }
public set(id string, balance float) { ... }
}
One approach might be to create a test for get
and a test for set
. Each test checks the content of the cache
hashmap to verify that these methods work correctly. For example, with a test on get
:
void testGetBalance() {
val customer = newCustomerBalance();
customer.cache.set("foo", 42); // Update the internal cache
val balance = customer.get("foo"); // Query the get method
// Assert that balance equals 42
}
This approach tightly couples the test to implementation details. Why? Suppose that later, we decide to change the underlying data structure (e.g., switching from a hashmap to a B-tree for performance reasons); we would likely need to update our tests to accommodate these internal changes, even though the overall behavior hasn’t changed. This tight coupling can make our tests fragile and harder to maintain.
Now, let’s look at a different way to test the same class but focusing on the behavior of the CustomerBalance
class:
Keep reading with a 7-day free trial
Subscribe to The Coder Cafe to keep reading this post and get 7 days of free access to the full post archives.