There are at least a couple of ways to take that. There’s the temporal definition of first, meaning you should write your unit tests before you write the code that makes the tests pass. That’s pretty straightforward. Write a set of tests that verify the functionality you’re about to build. Write some more tests that verify failure modes. When all your tests pass you’re (almost) done. That’s classic test driven development, or as I like to call it, design by example. It’s not perfect, but when you’re done you have working code, proof that it works, and some examples of how it’s expected to be used. If that’s the only way you interpret “Write your unit tests FIRST” you’ve gotten a lot of benefit.
But you can get even more by writing your test FIRST. That’s FIRST, the acronym. It’s not a TLA, but it’s close.
- Fast : Unit tests need to be fast. You’re going to be running them hundreds (thousands) of times. Even more if you’re using Continuous Testing. The longer they take the more intrusive they are, or worse, they start to get skipped, which eliminates much of the benefit
- Isolated : Each test should stand alone. It shouldn’t require any other test runs first, or set the stage for the next test. Ideally all the tests can run in parallel with no change to the results. They should be fully isolated from external systems, and if some kind of resource is needed it should be created by the test before it runs. Finally, the test should test one and only one thing. Test the happy path. Test a specific failure mode. Test initialization. In the limit there’s only one assert on the result. Not always possible, but a good goal.
- Repeatable : Each test should give the exact same result every time. Partly that comes from being isolated, but also includes handling of threads, race conditions, setup/cleanup, and (pseudo) random numbers. All flaky tests do is waste time. First you waste time figuring out it’s a flaky test, then you’re wasting time running tests that don’t mean anything.
- Self-Verifying : Unit tests either pass or fail. And they pass or fail as a group. They can’t require someone to look at the results and decide if it’s pass or fail. If there’s something important to the result in the console or log output then capture it and look at it. No asking a human to decide. There’s no such thing as needing 80% or better passing to call it success. No, "test A or B can fail, but not both". If that’s really the way it is then write your tests such that a single test passes or fails based on that criteria.
- Timely : Tests are written in a timely manner. There’s really two benefits here. You get the design by example from above, but you also make sure your code is testable. Sure, you can always refactor the code to make it testable, but why? If you do it that way the first time you don’t have to go back and redo it. There’s going to be a lot of re-work as you learn more anyway, so why add to the burden?
So before you go diving into some code, either to fix a bug or implement something, write the tests FIRST.