Thinking about tests: assertions and matchers

petalmind1 pts0 comments

Thinking about tests: assertions and matchers

zverok on lucid code

SubscribeSign in

Thinking about tests: assertions and matchers<br>Why some of us are still bothered about the way we write tests and what the “matcher” concept has to do with it.

Victor Shepelev aka zverok<br>Aug 16, 2026

Share

Read it on my blog<br>During most of my Ruby career, I was that unpleasant person who honestly enjoys writing tests and is frequently concerned about the ways we write them.<br>This means treating unit tests like the rest of the codebase: like something that is supposed to be read by humans and something that should be written efficiently and expressively. Basically, like something that wouldn’t be boring and disgusting to read and write.<br>This also means that I find it useful, once in a while, to stop and reflect on why we write test code the way we write it. And can this be improved?<br>Let’s move the elephant in the room from our way at once: from my point of view, this way of thinking does not become obsolete due to AI agents, who “can write any number of tests without being bored.” If anything, short, readable, and expressive code means more in this age. I extend this argument a bit in the last section of the post.

So, thinking about “how do we write tests” leads to a mass of related, tightly intertwined questions: How does the typical test in the codebase look? How hard is it to write a new one? How hard is it to read and maintain an existing one? How does it affect the overall codebase maintainability?<br>And how the design of the test framework and its utilities affects all these considerations and is affected by them?<br>To understand how this way of thinking might be useful, let’s look at the lowest level of the test: just one check, or assertion.<br>Starting from the beginning

Let’s perform a small “from the first principles” journey (bear with me!).<br>How do we check that the code we just wrote does what expected of it?<br>It starts easy when you have just a small amount of new code to test: one script, one utility function, one small class, things like that.<br>The first, naivest approach is to just run the code, see what it outputs (prints to the console, renders in browser, or makes any other user-visible effect), and compare it visually with what you’d like it to output. Frequently, this is enough for a quick prototype or a throw-away script: just write the code, run it, say “aha” or “oh no,” tinker a bit till you are happy, and then move over.<br>Obviously, it becomes tiresome for any non-trivial code, or one that turned out to be not short-lived: you eventually need it to do more and more things under more and more circumstances, and just manually checking “my new case is working, and the old one is not broken” becomes a burden.<br>And so you need some kind of a “test script,” with “when we run it like this, that happens” codified. This “check what happens” should be easy to write, and it should provide useful feedback: “in this part of the test script, this assumption turned out to be incorrect.” This is what we frequently call “test assertion”, “test check,” or “test expectation,” depending on the context and tools used.<br>The API to assert things is one of the first services that any test tool provides. And one that, in my opinion, affects the test library usage and developer’s thinking process.<br>Of course, we can go to higher levels to think how we organize many tests and groups of tests. And also how do we run them – a lot of decisions can be made here: order of tests, their independence, running in parallel, rerunning only a subset. All of this unquestionably affects our thinking, the design of our tests, and the design of our software. But it all starts with one test – and one assertion.<br>Not everyone considers “how do we write one test” to be of any importance. In the “architecture-first” thinking, the particular code at the level of singular “paragraphs” and “phrases” – its brevity, expressiveness, or ease of modification – is frequently brushed off as insignificant. My way of thinking on ease of development and maintenance of software, though, gives this “low” level significance. I will follow this line of thinking for now without further argument (which I expressed many times already). And I ask you to be with me here, if only out of curiosity, “how some of us approach what they do.”

So: a single assertion

The simplest of such APIs is assert(expression), with expression expected to return either truth/truthy value (the test passed) or false/falsy value (the test failed).<br>Frequently, this assert is even a part of the language itself, or its standard library – to be used as a debug or production guard against “impossible conditions.”<br>In testing, it might be used like this (usual “arrange, act, assert” structure)1:<br>arguments = prepare_arguments() # arrange<br>result = execute_code(arguments) # act<br>assert(result == expected_value) # assertHere, only the last line has any calls that should be provided by a test library. Or, if...

test tests thinking write code like

Related Articles