⏴ Home

Declarative testing with state assertions?

Oct 19, 2024

Tests are written "imperatively", i.e. with step by step instructions, like this:

test("increment steps by one", () => {
    // Config
    const counter = new Counter(0);
    // Followed by n instructions
    counter.increment();
    // Followed by n expectations
    expect(counter.value).toBe(1);
});

Config, instructions, expectations. We're telling the test runner exactly what to do and what to check. This is fine for simple tests, but as the number of tests grows, it becomes harder to read, and (hopefully!) comes with a lot of human-readable comments about why suddenly we expect value to be 1 -- or whatever.

Here's the thought: I guess you could imagine a more declarative way of writing tests:

when(counter.increment)
    .isCalled()
    .expect(counter.value)
    .toChange((prevState) => prevState + 1);

test("scenario", () => {
    // A bunch of weird instructions...
});

// ...More tests...

As I write this out, I'm not even sure exactly what I'd describe this as.

This is circling around "property-based testing" (see js/fast-check), but I'm not sure that's quite right. I think of property-based testing as a configurable way to generate inputs as a unit test.

What I'm describing is a little bit more like a assertion that is true throughout the lifecycle of the test regardless of when the state transition happens.

I think this is a really interesting pattern, and I'd like to explore it more in the future, even thought right now it's just a thought experiment since I don't really know the other formal tools for state machine type coding.