Testing Knowledge Isn’t (Just) a Testing Skill

The first and second posts in this series were about one person occupying two roles on the same project, sometimes switching between them within a single bit of work. That’s interleaving, and it works because the switch is cheap: same person, same context, same code sitting right there to look at. But most of the testing world doesn’t look like that. Most of it looks like two different people, on two different sides of a task, who never actually swap seats. I want to look at what “the role, not the person” demands in that arrangement: not interleaving so much, but cooperation. And I want to do it with something small enough to show in full.

If you haven’t read the earlier posts in this series, the short version: I treat “developer” and “tester” as roles a person occupies, not identities. The previous two posts showed what it looks like when one person switches between them rapidly, on the same code, in the same sitting. This post asks what changes when the switching isn’t rapid. When it’s two different people who each only get to do their half.

The example this time isn’t from the Z-Machine project. I wanted something small enough that the whole thing (function, test, and the reasoning behind both) fits in one sitting, with no spec document standing behind it. So: a retry function. The kind of thing that shows up in almost every codebase that talks to a network, usually written in about fifteen minutes and rarely looked at again.

Look at the signature. Two of those parameters, sleep and rng, have no reason to exist from a pure correctness standpoint. In production, this function is always going to call time.sleep and random.random. Hardcoding them would work. Ship it, and it retries correctly, backs off correctly, jitters correctly, every time.

So why parameterize them?

Because somebody, at the moment this function was written, was already asking a tester’s question: how would anyone verify this without an actual stopwatch and an actual coin flip? Without that question, the honest options are a test that really sleeps (slow, and flaky the moment your CI runner is under load) or a test that monkeypatches time.sleep and random.random globally, which works until two tests do it in the same process and start fighting over global state.

Notice what kind of knowledge that is. It isn’t testing knowledge in the sense of “know pytest.” It’s a design instinct (seams belong at the places where a function touches the outside world) that happens to only pay off if you already know, before you’ve written a line, that someone downstream is going to need to hold time and randomness still. A developer who has never had to write a test for code like this might reasonably not think to add those parameters. Nothing forces the thought. The tests will simply be worse, or slower, or flakier, and there will be no error message telling you why.

Now let’s consider the tests for the above code.

Specifically the two to focus on are these:

  • test_retries_until_success_within_the_limit
  • test_computes_delay_by_the_exact_formula

The first one only requires knowing what the function is supposed to do: retry on failure, stop on success, stop trying after the limit. You could write it from the docstring alone.

The second one is different in kind, not just detail. To write that test, whomever wrote it had to know the exponential backoff formula well enough to compute 0.1 * 2 ** (3 - 1) by hand, and had to know that pinning rng to 1.0 removes jitter from the picture entirely rather than just narrowing it. That’s not testing knowledge. That’s algorithm knowledge, borrowed. A tester who treats this function as a black box (inputs go in, retries happen, eventually it succeeds or raises) can write real tests. But they can’t write this test, and this is the one that would catch someone writing 2 ** attempt instead of 2 ** (attempt - 1), an off-by-one in the exponent that every black-box test would sail past because the function would still, eventually, succeed.

What Does This Tell Us?

Here’s what I think is the actual difference between this post and the last two. In the Z-Machine posts, I was one person switching hats mid-task: developer for a moment, tester for the next moment, sometimes both within the same design decision. Nobody had to know anything in advance; I just had to be willing to keep asking the other role’s question.

Nothing here in this post required a hat-switch. The developer who added sleep and rng to the signature didn’t stop and become a tester partway through. Instead, they wrote it that way from the first draft, because they already carried enough of the tester’s discipline to know what shape the function would need to have. And the person who wrote the exact-formula test didn’t stop and become a developer. Instead, they wrote it because they already carried enough algorithmic literacy to know what an off-by-one in an exponent would look like, and how to build a test that couldn’t miss it.

That’s cooperation instead of interleaving. Two roles, maybe even two different people, who never trade seats. But each one is carrying a working knowledge of the other’s discipline as a precondition, not an accommodation. Take that borrowed knowledge away from either side and something real breaks: an untestable function on one end, or a test suite that agrees with a bug on the other. “Developers should test better” and “testers should understand the code” are usually offered as separate pieces of advice, aimed at separate people. This function suggests they’re the same requirement, looked at from two different chairs.

Share

This article was written by Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.