Who You Gonna Call

5 min read Original article ↗
A picture of Petunia, from the 1950s children's book. She is proudly clutching under her wing a copy of the book xUnit Test Patterns.

Petunia has never actually read xUnit Test Patterns.

The 1950 children's book Petunia by Roger Duvoisin tells the tale of a goose who finds a book in a meadow. She clutches the book under her wing and proudly struts around the farm for all the other animals to see how smart she is. Puffed up with false confidence, she doles out terrible advice which her fellow farm animals implement to their own harm.

GenAI is fast but sloppy

Generative AI is like Petunia. When it comes to software quality, it confidently emits bad advice.

The book xUnit Test Patterns1 was published on January 1, 2007. Thanks to this book and its blessed author Gerard Meszaros, we've had a handy checklist of test smells for nearly twenty years. We know which design choices make software hard to verify and which quality control choices make software resistant to change.

In 2026, these are firmly established principles, yet their use is far from commonplace, and generative AI perpetuates the omission. In many recent pull requests, I encountered Claude-generated code like below. On one of these PRs, I invoked ten review agents, a mix of Claude and Codex. I didn't provide them any extra knowledge of good test practices. None of the agents pointed out the deficiency I am about to reveal to you.

Avoid behavior verification

Take a look at this code.

class FakeCache<T> : ICache<T>
{
    public int FetchCallCount { get; private set; }

    public T Fetch(int id)
    {
        FetchCallCount++;
        return default;
    }
}
[Fact]
public void GetById_DoesNotUseTheCache()
{
    // Arrange
    var cache = new FakeCache<Widget>();
    var service = new WidgetService(new FakeClient(), cache);

    // Act
    service.GetById(id: 1);

    // Assert
    cache.FetchCallCount.ShouldBe(0);
}

What's wrong with this test? It's asserting an interaction where it should be asserting a contract.

    cache.FetchCallCount.ShouldBe(0);   // asserts an interaction

The public contract of GetById is that it always returns fresh data, but what we've tested here is whether it calls Fetch. This is also called behavior verification2 because it verifies what the code does rather than what it achieves.

The consequences of behavior verification are well understood:

  • It makes the test brittle. It relies on facts that happen to be true right now but are not guaranteed to remain true.
  • It makes the code under test resist change. Whether you want to rename Fetch, add a parameter, or make it async, every test that asserts on it turns red.

As Mark Seemann wrote four years before ChatGPT3:

The experience that most people seem to have, though, is that when they change something in the code, tests break. This is a well-known test smell. In xUnit Test Patterns this is called Fragile Test, and it's often caused by Overspecified Software...the tests are highly coupled to implementation details of the software. The cause is often that...test verification hinges on how the System Under Test (SUT) interacts with its dependencies.

Prefer state verification

You can avoid the extra maintenance burden. State verification relies on public contracts rather than private implementation details. It compels you to design modular code which depends on guarantees rather than coincidences.

Sometimes there isn't any choice and we have to resort to interaction testing, but in this case, there's a natural state we can verify: the return value of GetById. If it returns a stale value, the test fails.

Here's a redesigned FakeCache:

class FakeCache<T> : ICache<T>
{
    private readonly Dictionary<int, T> _entries = new();

    public void Set(int id, T value) => _entries[id] = value;

    public T Fetch(int id) => _entries.TryGetValue(id, out T value) 
        ? value 
        : default;
}

Now after calling GetById, instead of counting invocations, the test asserts what the SUT returns:

[Fact]
public void GetById_ReturnsFreshData()
{
    // Arrange
    int id = 1;
    var cache = new FakeCache<Widget>();
    cache.Set(id, new Widget("Stale"));

    var service = new WidgetService(
        new FakeClient { FreshResponse = new Widget("Fresh") }, 
        cache
    );

    // Act
    Widget result = service.GetById(id);

    // Assert
    // We'd see "Stale" if the data was not fresh
    result.Name.ShouldBe("Fresh");   
}

The reward for this discipline is the test won't break unless the public contract of GetById changes.

What to do now

When you go to a restaurant, you don't care whether the chef cooked your meal in one pan or in five. You care that the food is delicious.

By the same spirit, avoid behavior verification. Run away from code that resembles Assert(Thing.WasCalled(Times.Once)).

Instead, prefer state verification. Stick to public contracts. Assert on return values or mutated state. Let what other functions your function calls remain a secret to your function.

Now that you know the difference, you can choose. Beware though, you have to be vigilant. I constantly catch Claude trying to sneak in behavior verification.

For Petunia, things start to go well again when she actually opens the book and reads it. Your reward for exercising good test discipline is the delight of software that is easy to change.

A picture of Petunia, from the 1950s book. She is poring over the contents of the book xUnit Test Patterns.

A developer learning that testable code is a joy.

References

  1. xUnit Test Patterns
  2. Behavior Verification
  3. From interaction-based to state-based testing

Image Credits

Petunia is a character created by Roger Duvoisin in Petunia (Alfred A. Knopf, 1950). Illustrations © Roger Duvoisin. The Knopf imprint is now part of Penguin Random House.

The book cover is from xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros (Addison-Wesley, 2007). Cover © Pearson Education, Inc.; Addison-Wesley is an imprint of Pearson.

Both images were edited and upscaled with AI.