Epistemic Testing, Chapter 01 – What Makes a Test a Test?

“The purpose of a test is not to prove correctness, but to reveal understanding.”

We write code to shape ideas into form, but what we really wrestle with every day are the invisible things: assumptions about business logic, intentions buried in variable names, and expectations about how systems will behave when no one is watching. Code is a snapshot of thought, a frozen hypothesis about how reality should work.

Testing is how we thaw that snapshot 😁 and see whether it still holds true. It’s not just about detecting mistakes; it’s about asking, Did I truly understand what I meant when I wrote this?

When a test passes, it doesn’t simply mean the code works, it means your mental model and the system’s behavior are momentarily in harmony(read it again, carefully). When it fails, it’s not a punishment;  it’s the system revealing where your understanding stops matching reality. It’s teaching you where your mental model breaks down(read the whole again).

Read the Chapter 2

So what really makes a test, a test?

It’s not the framework, the mocks, or the green checkmarks.  It’s the act of turning belief into evidence.A test is an executable form of curiosity, a precise, repeatable question we ask our code to reveal the truth behind our assumptions.


Introduction

We spend our days as developers dealing with invisible things, such as assumptions about business logic, intentions hidden in variable names and expectations about how systems will interact when we’re not looking. Code is merely the transcription of those thoughts; a fragile surface that captures our reasoning at a particular moment in time.

When we test, we’re not just checking behavior; we’re asking the system, the software or the code:  Did we truly understand what we meant when we wrote this?

Complicated and complex systems come with ambiguity. Every system we build begins in ambiguity. Testing, at its best, is how we carve clarity out of that fog. It’s not about proving correctness in the mathematical sense; it’s about exploring what correct even means for the problem at hand. Tests are our dialogues with the system; small, executable fragments of thought that reveal where our understanding is incomplete, or where our design no longer fits its intention.


Are We Really on the Same Page About Test and Testing?

From the zero day of the software industry, the most used terms by people are test and testing, as a means or a function(in the math term) that takes a piece of code as input and returns a binary truth (pass or fail) which finally tells whether a piece of code is working as it is supposed to or not. Simply, We say: Run the tests, see if it works.

But let’s pause for a moment.

What is the test, really? Is it the script we wrote? The line that asserts equality? The runner that executes it?

In its essence, a test is a design, a piece of logic, a function,  a human construction, a small, deliberate piece of reasoning, no different from any other function in your program. It embodies a hypothesis: Given this input, I expect this behavior. It begins as thought: If this system behaves as I expect, then under these conditions, this should happen. That reasoning becomes structure, an algorithm, a body of logic, that examines another piece of code(we tend and love to call it production code, so, hmm, okay, I’d call it production code too). The only difference is intent: while production code aims to perform, test code aims to examine.

Testing, then, is the act of running that function. If a human performs it, clicking, observing, reasoning, we call it manual testing. If we want the machine to do it for us, we encode that same intent into another piece of code, written in a programming language.

When a human executes a test directly, step by step, by observing the system and checking outcomes, we call that manual testing. When the same reasoning and intent is translated into a programming language, encoded so the machine can perform it, we call that automated testing.

Nevertheless it’s manual or automated, the design is always done by a human. The execution can be done by a human or machine.

So the test can become executable though, a small algorithm that observes another algorithm. And when the machine runs both, something beautiful happens: understanding itself becomes automated.


The Role of Test

But that definition is small. It ignores the deeper role of testing as an act of inquiry.

A test is not only a tool to confirm correctness, it’s a lens that lets us examine the hidden assumptions embedded in our code. Testing is the process of making intentions explicit, of translating human ambiguity into something precise enough for a machine to verify.

When we look at testing this way, it stops being a phase or a safety net, and becomes a way of thinking. It’s how we explore design, refine language, and build trust, not only in the system, but in our own understanding.

In this book, when I say testing, I don’t mean only unit tests or automated checks. I mean the full spectrum of inquiry that bridges thought and execution:

  • Asking questions through code.
  • Observing how a system responds.
  • Using those responses to shape architecture, refine meaning, and evolve understanding.

Testing, then, is not about certainty, it’s about curiosity disciplined by precision.


Why We Misunderstand Testing

We misunderstand testing because we mistake verification for understanding. For decades, our industry has spoken about testing as a safety measure, a net that catches mistakes after the act of creation. We treat tests as a separate layer, something secondary to real code. But what if tests are not afterthoughts to development, what if they are expressions of thought itself?

Most arguments about testing, unit vs. integration, TDD vs. BDD, mock vs. stub, are not really about software at all. They are about philosophies of truth: how we decide what it means for something to be right. Some people believe truth lives in the smallest piece, If every function works, the system works. Others think truth only emerges from interaction, It only matters when it all works together.  Neither side is wrong. But both are incomplete.

Testing is not a side of an argument, it’s the space between them. It’s where intention meets observation. A test doesn’t just ask, Does this work? It also whispers, What did I really mean when I wrote this?

The act of testing transforms code from something we wrote into something we understand. And that transformation is what makes great software architecture possible.


The Many Faces of a Test

Every test tells a story.  Sometimes that story is small and immediate, “This function should return 5.”  Other times it’s expansive,  “When a user places an order, the system should send a confirmation email.”

Between these extremes lies an entire spectrum of thought. Each type of test, unit, integration, acceptance, exploratory, is simply a different scale of curiosity. The smaller the test, the more microscopic our curiosity.  The larger the test, the more we question the system’s purpose. There are many forms of testing out there, and I don’t plan to deal with all of them here. What matters is the curiosity behind them, the kind of question each one asks.

A unit test looks at a single piece of code in isolation and asks, Does this part behave the way I intended? It helps us clarify our thoughts at the smallest scale. Writing a good unit test is like holding a magnifying glass over one idea and seeing whether it holds up under close inspection.

An integration test steps back and asks, Do these parts work together as I designed them to? It’s less about the pieces themselves and more about the relationships between them, the boundaries where ideas meet. Integration tests teach us discipline at those edges, where most real-world problems tend to appear.

An acceptance test looks from the outside in. It asks, Does this system meet the expectations of the person who uses it? This kind of testing moves us closer to empathy, it reminds us that software is not built for the code, but for the human at the other end.

An exploratory test begins with uncertainty. It asks, What don’t I know yet? Instead of confirming behavior, it invites discovery. These tests are less about proving and more about learning; a structured form of curiosity that often reveals what no automated check would.

Now, I should say: I’m not an advocate of separating tests too strictly into these categories. In real work, the lines are blurry, and that’s okay. What unites all forms of testing isn’t the framework or format, but the intention, to learn something true about the system. A good test is not an answer; it’s a question, expressed in code, that helps us understand what we’ve really built.

What unites them all is not format or tooling, it’s the intent to learn. A good test is a question, not an answer.  It’s a hypothesis expressed in executable form.


Example: The Question Hidden in a Test

// "Do I understand what 'available' means in this domain?"

test('Product availability should depend on both stock and reservation', () => {

  const product = new Product({ stock: 10, reserved: 8 });

  expect(product.isAvailable()).toBe(true);

});

This isn’t just checking a condition, it’s exploring a definition. If tomorrow availability changes because of a new business rule, the test will fail, not because the system broke, but because our understanding evolved. The failure is feedback from reality, a moment of learning.


From Verification to Exploration

When we see tests only as verification tools, we reduce them to guardians of correctness,  rigid, repetitive, and eventually ignored. But when we treat them as instruments of exploration, they become alive. Each test becomes a tiny experiment that refines both the system and mind.

Testing, then, is not the art of proving software right. It’s the discipline of continuously questioning our assumptions, until what remains is something both meaningful and resilient.


Tests as Bridges Between Thought and Reality

A test is the proof-of-work for an idea. It takes a mental assumption, brings it out into the open, and makes it observable, measurable, and verifiable. It forces us to define a clear boundary between what we think we built and what the code actually does.

Testing, then, isn’t just about whether a feature passes or fails. It’s a fundamental act of clarifying understanding and shaping design. Every test is a conversation, a challenge, a moment of self-dialogue with the system you are building:

“I believe this function behaves this way under these conditions, let me run a tiny, repeatable experiment to see if reality agrees.”

If the test passes, your understanding is confirmed. If it fails, the test has done its job: it has immediately highlighted a gap between your thought and the code’s reality.

Every test is a conversation, a challenge, a moment of self-dialogue with the system you are building

Illustration: The Tea Mug Test

I’m a tea-drinker and tea mug lover, that’s why I use this example everywhere 😀. Imagine you are a product engineer designing a brand-new Tea Mug. A crucial requirement is safety: The handle must remain cool to the touch even when the mug is full of boiling liquid. This is an assumption about your design.

To verify this, you design and write a Test Procedure (a manual test):

  1. Arrange: Boil water. Ensure the ambient temperature is normal.
  2. Act: Pour the boiling water into the mug and wait for exactly 20 seconds (simulating the time a user might take to pick it up).
  3. Assert: Touch the handle. Measure the handle’s temperature using a thermal scanner. Expect the temperature to be below 50 fahrenheit(or whatever safe threshold you’ve defined).

Each step in this procedure isolates and verifies one single assumption, the thermal resistance of the handle. You don’t test the mug’s color or its capacity at this moment; you focus on one verifiable proof.

Software testing is exactly this, but automated. Instead of pouring water, we call a function. Instead of touching a handle, we run an expect()/assert() statement. This automation provides fast, reliable feedback about a single assumption, instantly proving or disproving our idea.

In Action: Automated JavaScript Proof of Work

Let’s see what this looks like in real life.
Imagine a short conversation between a developer and a product manager. They’re sitting together, trying to pin down what adding an item to a cart really means.

Domain Expert: When a customer adds an item, the cart should show one more thing, that’s how they know it worked.
Developer: So, if it starts empty and we add a tea mug, the total should be one. That’s the visible proof.
Domain Expert: Exactly. The system should behave as if it’s confirming the user’s intention.
Developer: Alright. Let’s turn that agreement into a test, a small experiment the computer can run to make sure our understanding holds true.

That conversation, that brief meeting of minds; is the real beginning of a test.  The test doesn’t start in code; it starts in language. It begins as a shared expectation about behavior, discovered through dialogue. When we write a test, we’re turning that shared expectation into an executable proof of work; something a machine can replay, over and over, to make sure our system still behaves the way we intended.

As mentioned above, the first real task is to translate the design conversation into code, to script the test itself as the foundation of the work. In other words, we write the proof of work before the work itself. The test becomes our executable understanding, ensuring that what we build continues to reflect what we agreed upon.

Here’s how that conversation might become code using Jest, a JavaScript testing framework:

// cart.test.js

const Cart = require('./cart');

describe('Cart Functionality', () => {

  let cart;

  // Arrange: Prepare a new cart before each test
  beforeEach(() => {
    cart = new Cart();
  });

  test('starts empty', () => {
    // Our shared assumption: a new cart should contain no items
    expect(cart.numberOfItems()).toBe(0);
  });

  test('adding a single item increases the count by one', () => {
    const initialCount = cart.numberOfItems(); // expected to be 0
    cart.addItem('Tea Mug'); // the action under discussion
    expect(cart.numberOfItems()).toBe(initialCount + 1); // proof of work
  });

});

And here’s the simplest implementation that makes this conversation true:

// cart.js

class Cart {
  constructor() {
    this.items = [];
  }

  addItem(item) {
    this.items.push(item);
  }

  numberOfItems() {
    return this.items.length;
  }}

module.exports = Cart;

The conversation has now become code, an executable dialogue. It captures the shared intent between the Domain Expert and the developer. The computer simply plays back that conversation to confirm whether reality still matches what both of them agreed upon. Every time this test runs, it replays that understanding:

“When we add an item, the cart’s count increases by one.”

If this truth ever stops being true, the test fails, not as punishment, but as an alert: our shared understanding has drifted from reality.

This is how tests are discovered and designed:

  1. Start with a conversation, what behavior matters to the user or the business.
  2. Turn that conversation into a concrete, observable expectation.
  3. Encode that expectation as a test, your proof of work.
  4. Let the machine replay that proof endlessly, ensuring alignment between intention and implementation.

In the coming chapters, we’ll explore how this pattern scales beyond simple examples; how tests evolve into living design conversations, how they uncover missing boundaries, and how they help teams see architecture not as structure, but as shared understanding made executable.

A good test isn’t just a technical check; it’s a conversation turned into code, a permanent, repeatable dialogue between people, preserved in logic.


Language as the Medium of Design

Language is not decoration; it’s design material. The way we name things, addItem, numberOfItems, Cart,  reveals how we see the world.  When our words are vague, our models become fragile. When our words are sharp and shared, our systems become understandable.

Testing forces language to become concrete and precise. A test must be unambiguous, it cannot rely on hmm, sort of, probably, or it should kind of work. It has to express behavior clearly, in a way a machine can verify and a human can read. That’s why many of the deepest modeling insights arise not while coding, but while naming tests.  A well-named test is a small act of discovery, it exposes what we truly mean.


Dialogue as the Source of Modeling

Every model starts as a conversation, not an architecture diagram. As we saw above, It begins when people try to describe the same idea in words, and realize where their understanding diverges. Tests capture those moments of alignment.

In that sense, testing drives modeling. When we write a test, we’re asking: What do we believe this system should do? To answer, we must agree on terms, behaviors, and boundaries, the same agreements that shape our domain model.

The test, then, is not a check after design. It is the design itself taking form, the first executable version of shared understanding. The model is born from dialogue; the dialogue becomes a test; and the test, once encoded, becomes the design’s proof of work.


From Dialogue to Design

Tests, when viewed this way, are not secondary artifacts, they are the first visible design of the system. They are where the model emerges.

When people talk about how a feature should behave, they are performing exploratory modeling through language:

  • What do we call things? (Vocabulary)
  • How do they behave? (Rules)
  • What should happen under certain conditions? (Scenarios)

Out of that dialogue, a domain model begins to take shape. The test then becomes the first executable form of that model. It is both a record of understanding and a guide for implementation.

In this sense:

  • Dialogue is the source of modeling.
  • Tests are the drivers of design.
  • Language is the bridge between human intention and machine precision.


That’s why the words we use in our tests matter. A test written in clear, domain-aligned language doesn’t just verify behavior, it teaches the system’s purpose to anyone who reads it.

A good test is not born from syntax, it’s born from a conversation. The code simply remembers it.

There are sooo many things we could talk about 🤯, but alright, I’ve gotta wrap this chapter up. I’ll be back with more later, but for now, here are a few fun practices to try.


Practice in Action

The philosophy of testing means nothing unless it’s lived in code. Start small, and let curiosity drive you.

Exercise: Isolate and Prove

Pick a small function in your project, maybe something like formatPrice() or capitalizeName().
Ask one simple question about it: What do I expect to always be true? Then write a test that proves or disproves that single assumption. If it takes more than a few lines, your assumption might be too broad. Narrow it. Make it testable.

Mini Case Study: Document Fragility

Find the module everyone fears to touch, the one that just works, mostly. Before changing anything, write tests that document its current behavior, even the ugly parts. Passing tests will become documentation. Failing tests will reveal what’s broken. You’ll be creating safety before you create change, a living record of how things actually behave.

Thought Experiment: The Cost of Uncertainty

Imagine having to modify a critical feature but being forbidden to run any tests for a week.

  • How much slower would you move?
  • How much anxiety would that create?

That hesitation, that invisible drag, is the cost of uncertainty.Tests eliminate it by replacing speculation with evidence.


Chapter Takeaway: Clarity Made Concrete

Testing is not a chore or an afterthought. It’s how ideas earn the right to exist in code. When we test, we take something uncertain, a belief, an assumption, a conversation, and give it structure, boundaries, and repeatable truth.

  • Tests begin as dialogue, the meeting point of people, language, and intent.
  • They grow into models, shaping how we see the domain.
  •  And they end as design, executable and verifiable.

In short:

A good test is clarity made concrete, a conversation turned into code.

As we move into the next chapters, we’ll explore this process more deeply:

  • How dialogue and naming form the backbone of design.
  • How tests drive models forward, revealing missing concepts and fuzzy boundaries.
  • How testing becomes not just a practice, but a way of thinking.

Testing is not about proving software right, it’s about proving we understand it. Each test is a moment of alignment between what we think we built and what the system actually does. And when those two align, even for a moment, we have something rare in software: truth we can trust.

Leave a Reply

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