Two strategies to organize your unit tests
An experienced data engineer asked:
"How do I organize these unit tests? Sometimes I am just not sure where to start."
He asked this because he's working on meaty problems. All the beginner tutorials make it look easy, because they show you tiny toy problems whose code fits inside a phone screenshot. But real software systems are complex, with sharp flying moving parts, and lots going on. If you are writing a lot of code that does not have unit tests yet, how do you go about it?
There are two strategies.
The first strategy is easy to start. You have a simple mapping:
- Each Python module of your application gets its own test module. So something.py has its tests in test_something.py.
- Each class in that module gets a testcase class in that test module. For example, if your program has a Profile class in userinfo.py, then there will be a TestProfile class in test_userinfo.py.
- Each method of that class gets a method in the test class. If your Profile class has a method call in_group(), then the TestProfile class gets a method called test_in_group(). These test_* methods contain your assertions, checking the behavior of that method.
- What about bare functions? My data engineer friend had this question. In your test module, make a special class called TestMain or TestFunctions or whatever - the name isn't important. And that class has test methods, one for each function. So if your userinfo module has a function called normalize_username(), your TestMain class will have a method called test_normalize_username().
The advantage of strategy #1 here:
It lets you get started with no effort.
There's no hard thinking required. No "writer's block"; it is almost mechanical. You just look at the modules, classes, methods and functions you have, and start writing your tests.
That's the great thing about strategy #1.
And as you fill out your tests this way, you naturally bridge to strategy #2:
Organizing the test code by behavior, not by code structure.
For example, in a marketing automation system I have built, incoming prospective customers are sorted into these categories:
- Accepted
- Tentative (meaning we need to talk to them first to decide if they are accepted or not)
- Rejected
The unit tests for that have test methods with names like test_accepted(), test_tentative(), and test_rejected(). Which do not map to any function, class or method; they are for higher level behavior.
Sometimes when you start writing tests, you will naturally think of these behavior categories, and write your unit tests that way from the start.
But I will tell you: whether I am creating a brand-new program from scratch, or writing tests for an application that should already have them, but does not...
Either way, I will often start with strategy #1. And let strategy #2 naturally work its way in, as I get more clarity on that higher level behavior I want the test suite to be validating.
To do any of this requires you are able to write unit tests to begin with. At least that you know the basics, enough to start applying them to real-world code. (As opposed to phone-screenshot-sized toy code.)