The default structure for Xcode projects has a directory to hold implementation files and another directory to hold test files. There is no standard for where files are placed in each of the directories, but often we try to create a mirror version of the source files in the tests directory. We propose that co-locating the test files with the implementation files provides a more seamless approach to development in an environment where a lot of emphasis has been put on testing.
Xcode projects with unit tests
By default, an Xcode project with unit tests has two targets, one for implementation files and one for test files, and two corresponding directories to hold the files. The two directories are purely for organizational purposes as any file in the project can be associated to any subset of targets in the project.
This style is also popular in “convention over configuration” environments, such as Rails and Maven. We find this approach to have many downsides.
- Maintaining a mirrored directory structure for tests is thankless work that feels like a chore.
- It can be difficult to quickly navigate between implementation and corresponding test file.
- It is quite common to browse code on GitHub and other code hosting sites these days, and the separation of test files makes it hard to see how well-tested a codebase is.
- Tests tend to become second-class citizens, often having a lower standard of code quality than implementations.
We propose to co-locate the test file in the same directory as the implementation file, helping to solve some of these problems. This immediately doubles the number of files in a directory, but makes it obvious where to find the tests for a particular file. Even though the test file has been moved, we still maintain the target it is associated with.
One less directory structure
No more hunting for where to find files or put files.
Implementation & test file navigation
Navigating between implementation and test files is becomes much easier now. More interestingly, the test files are kind of analogous to header files, except they are also living code that describe how the API is expected to behave.
The GitHub effect
Browsing large amounts of code on GitHub is quite common these days, and co-locating the test files makes this much easier. People can easily jump between implementation and test, and are practically encouraged to do so now. It also aids in reviewing pull-requests since the implementation and test files will alternate instead of having them lumped together.
Tests are first class
Tests should have some coding standards as implementation. Where we used to have a separate SwiftLint configuration for tests, we now use the same configuration for all code.
What about tests not associated with a single file?
There’s not always a one-to-one correspondence between implementation and test. Some tests cover code in multiple files (e.g. integration tests or UI tests). Tests of that type should structured in the more traditional way, inside a separate directory. We could even go further and say that those types of tests should even have their own target and be separate from your unit tests.
In our codebase
We are now all onboard with this test co-location style. We have opened multiple pull-requests (by the way, we’re open source!) to bring each of our first-party dependencies (link, link, link) and the main app (link) into this world, and so far we’re loving it!
If any of this sounds interesting to you, we’re hiring!