Long name, short body
Martin Fowler argues that functions should be as short as a few lines of code. I feel like that’s a bit extreme, and usually, my functions are longer than that.
It turns out my test functions are a different beast. Reflecting on how I write code, I noticed that my test functions are usually much shorter than my regular code. Not only are they shorter, but in general, they look different.
Long name, short body is a good rule of thumb for writing simple and maintainable Python unit tests.
Here’s how I do it:
- I give my test functions ridiculously long and descriptive names.
- I set your testing state mostly outside.
- I call an assertion.
My test function names usually start with “test_”, followed by a descriptive explanation of the behavior or property I’m testing, as close to plain English as possible.
In real life, it may look somewhat like this.
def test_discount_reduces_amount_when_applied(discount, product): assert product.apply_discount(discount) < product.price def test_discount_value_cant_be_negative(discount): with pytest.raises(ValueError): discount.set_value(-1) def test_discount_value_cant_be_larger_than_threshold(discount, product): with pytest.raises(ValueError): discount.set_value(DISCOUNT_THRESHOLD)
Ridiculously long names in regular code don’t look natural. With tests, it makes sense, though.
- You never call your test function explicitly. The testing framework does it for you. Don’t worry about names being too awkward typing repeatedly or your code being reformatted ugly because a few too-long function names don’t fit in a line.
- When the test framework calls your functions, it prints their name. Explanatory names help understand the context when a test runs or when it fails. Also, for some reason, I find it aesthetically pleasing and satisfactory.
While my regular functions look like a rectangle positioned vertically, functions written this way look more like rectangles lying horizontally on their side.
Here are a few techniques helping me keep the functions small and neat.
Split a long function. When you have a long test function with independent asserts, split it into separate functions and give them ridiculously descriptive names: a straightforward technique that takes minimal effort.
Extract helper functions. Developers remember to extract helper functionality into separate functions. Somehow, I noticed this rule is forgotten when writing tests, and people feel OK copying and pasting the setup instructions from one test to another. If you repeat the same initialization code over and over again, extract it into a helper function.
Turn helper functions into fixtures. A fixture is a pytest magical superpower. When you define a test function with parameters, for each parameter, pytest finds a fixture function with that name, calls it and passes the returned result as the argument. Not all helper functions need to be turned into fixtures, but in general, fixtures are more powerful and flexible.