The benefits of colocating unit tests
While a project is still small, we might divvy up the elements of our application by their designation. We put templates in
app/views, controllers in
app/controllers, and stylesheets in
app/assets/css. This works at first, but becomes unwieldy once the application grows in size. As soon as we have more than a handful of controllers and templates, files that are interconnected and only work in tandem are spread out over the entire directory structure. Over time, it becomes harder and harder to identify all parts that make up one specific feature.
Where this becomes most difficult is when a piece of functionality needs to be renamed or removed. Under this basic approach to structuring files, we now have to dig through the entire tree to find all pieces that belong to that one feature. Consistent naming can help with this, but it can often only carry us so far.
Assume our application contains a search form. Under the traditional approach, we could spread this feature out across our codebase by grouping its elements with others of the same purpose:
app/ ├─ controllers/ │ ├─ search-form-controller.js │ └─ … ├─ views/ │ ├─ search-form.html │ └─ … └─ assets/ └─ css/ ├─ search-form.css └─ …
As more features get added to our codebase, these directories will grow in size, and we will find ourselves with long lists of files that have little to do with each other. Their only shared characteristic lies in the fact that they are of the same file-type and serve similar purposes. Locating a specific file becomes difficult, and will pretty much require the use of a “search and open”-feature in our editor of choice or enough willpower to comb through our entire application over and over again, only to eventually miss one file anyways.
Rather than spreading a piece of functionality out across several directories, we can instead combine all its parts in a single directory centered only around that one component:
app/ └─ components/ ├─ search-form/ │ ├─ search-form.css │ ├─ search-form.html │ └─ search-form-controller.js └─ …
Now that the basics are out of the way
What is missing from the example above is what this post is about in the first place: there are no tests, neither unit nor otherwise, in any of the examples. As not having tests is bad, we now have to figure out where to put those.
Frequently, tests are put in a
test/-directory in the root of our application, which would extend our directory structure to the following:
app/ ├─ components/ │ ├─ search-form/ │ │ ├─ search-form.css │ │ ├─ search-form.html │ │ └─ search-form-controller.js │ └─ … └─ test/ ├─ end-to-end/ │ ├─ searching.js │ └─ … └─ unit/ ├─ search-form-controller.spec.js └─ …
While this brings with it the advantage of grouping all tests in a single
test-directory, we are once again faced with our original problem: files that are directly connected to a single feature are spread out across multiple directories.
We need to differentiate between the kinds of tests outlined in the assumed directory structure above, as they fulfill dissimilar roles. As unit tests are based on a single unit of functionality, we can directly map them to “components” and should thus put them right with the units they are testing.
End-to-end and other kinds of tests potentially cover multiple “units” at the same time, so they do not follow the same pattern and have to be treated differently. While we should not get rid of the
test/-directory entirely, it does make sense to group unit tests with the remainder of the feature-specific files, leaving all others in the dedicated directory:
app/ ├─ components/ │ ├─ search-form/ │ │ ├─ search-form.css │ │ ├─ search-form.html │ │ ├─ search-form-controller.js │ │ └─ search-form-controller.spec.js │ └─ … └─ test/ └─ end-to-end/ ├─ searching.js └─ …
Through this, we are distributing unit tests across the codebase in exactly the same way we picked other elements of our various features and grouped them together. This way, all files that are relevant for a piece of functionality, leaving out broader types of tests, can once again be found in exactly one location.
One could argue that having all test-files in a dedicated directory means we are separating production code from development-only files. However, we are most likely using a build tool to prepare our files for their use in a production-environment anyways, which could be extended to undo that grouping if and when necessary. Particularly when using the component-based approach outlined here, we certainly have to process numerous files that we purposefully distributed across several directories. Ignoring test-files in these build-processes is not only possible, but also almost trivial.
Putting unit tests in a dedicated
test/unit-directory is the QA-equivalent of grouping all our application-files on the basis of the job they do. Sure, all of them will be in one single location, but the elements that make up a specific feature are spread out across the entire directory structure instead.