Raw capture â raw/composable-tests.md
url: https://newsletter.kentbeck.com/p/composable-tests date_fetched: 2026-08-21
The Test Desiderata desires 12 properties for tests, two of which are:
No, and hereâs why (I finally got an exampleâexamples are always the hardest part.)
If a test runs by first setting up its own test fixture, creating from scratch all the data it will be using as input, then that test is guaranteed to be isolated. It doesnât matter what order you run the tests, the results will be exactly the same. (This is the same property as referential transparency in functional programming.)
Isolation is encouraged in the xUnit testing frameworks (at least most of them) by creating a new instance of a test object for every test & running the setUp() function before running the test. (Some frameworks, notably NUnit, reuse test instances, opening the door to breaking isolation.)
Say we have a suite of isolated tests & we run them all together. The suiteâs success should give us confidence (be predictive in Desiderata terms), even though each individual test on its own isnât comprehensive.
Exampleâsay we have a test:
test1()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
We get that working so we want to implement the next bit of functionality. We copy, paste, & extend:
test2()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
actual2 := object.nowSomethingElse()
assertEquals(expected2, actual2)
I have seen tests like this that have been copied, pasted, & extended 6 or 7 times. That last test is pretty hard to read.
Notice that test2 canât pass if test1 fails. All non-compliant programs caught by test1 will also be caught by test2. We have at least 3 options that preserve the same coverage, the same predictability:
From a purely aesthetic standpoint (& donât discount aesthetics), leaving both tests as is offends my sensibilities. They are redundant! Something must be wrong.
Deleting test1 loses us another property from the Test Desiderataâtests should be specific. Thatâs the property of tests where, when one fails, you know exactly where the problem is.
Which leads to my preferred solutionâcomposition. I trim test2 to avoid the purely redundant parts:
test2()
object := new Whatever()
object.doSomething()
actual := object.nowSomethingElse()
assertEquals(expected, actual)
The composition of test1 + test2 hasnât lost any of the predictive property. It hasnât lost any of the specific property. In fact the composition may be more specific as it is possible for test1 to fail & test2 to pass (although they may both fail for a common reason).
Letâs say we have 4 ways of computing interest & 5 ways of reporting that interest. The brute force approach to testing this is 20 tests. Using composition, though, we can achieve the same confidence in our system with 10 tests. If the variants of computing interest are separated, in a functional programming sense, from the variants of reporting, then we need:
Gaining confidence from composed tests requires some thought, some inference, some design (to make the orthogonal dimensions demonstrably orthogonal), but the investment in writing pays off in making tests:
When Iâve explained what I mean by composable tests, I often receive shocked reactions from experienced testing-developers. âI would never reduce the assertions in a test.â This seems to me to be a reaction based in fear, not in principle. We worked so hard to get to write tests at all. We canât make them worse.
Composition isnât making tests worse. Composition is looking at the tests as a whole, trying to make the whole better as judged by several valuable properties of tests.
Boost your teamâs code quality and shipping speed with CodeRabbitâthe most advanced AI code review tool built for engineers. CodeRabbit delivers context-aware, line-by-line reviews, instant one-click fixes, and concise PR summaries, integrating right into your GitHub workflow so you spend less time diff diving and more time building.
Join thousands of developers whoâve halved code review time and defect rates with CodeRabbit. Start your 14-day free trial and experience seamless AI reviews, actionable feedback, and effortless codebase learning.
Ready to optimize your engineering workflow? (CodeRabbit is free for public repositories, with Pro features available for enterprise teamsâstart now to transform your code reviews)
Sponsored by CodeRabbit.
I really like this post, in particular the thoughts on N x M.
If a little bit of self-promotion is allowed, I wrote a blog post about a year ago that came to a similar conclusion. I think of it as a way to build robust test suites. In my ideal test suite, only a a single test would fail whenever an error occurred. I also think it could be linked to Separation of Concerns, Donât Repeat Yourself, or the Single Responsibility Principle.
Feel free to read my thoughts: https://henko.net/blog/verify-only-what-you-need/
Hi Kent,
my worry with the composition above is that if there is a bug in doSomething(), then the state in which we execute doSomethingElse() is unknown. Continuing might be generating errors in doSomethinElse(), which are actually not a bug in doSomethinElse() but the result of executing it in a broken state.
One alternative approach could be to make it possible in the test to explicitly establish the state that doSomething() is expected to bring about. E.g.,
object := new Whatever(readyForDoSomethingElse)
result := object.doSomethingElse()
Yet another alternative would be to explicitly assert on the state that we expect object to be in after doSomething(), eg
test2()
object := new Whatever()
object.doSomething()
assumeTrue(object.isReadyToDoSomethingElse())
actual := object.nowSomethingElse()
assertEquals(expected, actual)
Some test frameworks assign a special state to tests that fail because of a failed assumption, so we might know that test2() did not fail because nowSomethingElse is broken, but because the assumptions we made do not hold