Make unit tests discoverable
Unit and integration tests that are supposed to test a particular function, method, class, or module, should be discoverable. Developers need to see if and what kinds of tests were already written for a particular unit, navigate to them easily, or introduce new tests in a predictable location.
No matter what conventions we adopt, no mental energy should be spent in thinking where to write unit tests or how to name the test files, because otherwise we introduce unnecessary friction and waste time.
Therefore, there are three typical locations where to put such tests:
In the same code file as the code they are testing
In a test file placed right next to the code file with a predictable file name like test_<code file name>
In a test file placed in a folder tree with the same hierarchy used for code files, again with a predictable file name. That means that tests for src/folder/code.py would be in test/folder/test_code.py.
Single file scripts and some projects can get away with putting the tests right next to the code in the same file, if the particular programming language and test runner support it. The benefit is clear: there is no need to look for any other locations.
However, most projects opt for the two other strategies. Which one to pick? In the spirit of locality of behavior, the preference should be in a test file just next to the code itself, usually prefixed or postfixed with “test” or similar.
However, there are arguments against it too. Besides other technical limitations, test collection by a test runner can be faster when all tests are placed in the same parent folder without any other files. It is also easier to do a code search within test code only or exlude test code from the search, same as setting different linting rules, and so on.
Either way, when the code file <> test file relationship is clear, code editors and IDEs can leverage the naming pattern and allow developers to switch between code and test files easily. Visual Studio Code doesn’t seem to have a native way of doing so, but there are some attempts at doing that with Jump to Test or Go to Test extensions.
It helps to keep code files small, so that the corresponding test files are small too, otherwise, the tests for a concrete class or function would be lost among others. If the test framework you use supports some form of organization within test files, like putting all tests related to one unit in its own test class or other namespace, it is a good way to improve the developer experience.
Of course, existing unit tests can be usually found through the Find All References command, as unit tests typically invoke the particular “unit” of code directly. Sadly, not all code editors can filter the references to test code only. Find All References also doesn’t help with choosing a location for new tests or enforcing rules about test file placement.
All in all, thinking about test discoverability can reduce friction and mental load when dealing with unit and integration tests. It is typically not a hard thing to place tests in predictable locations, and the benefits are all worth it.
Petr