# [EN] TDD: The 3 things you can test

When you're just starting with **TDD**, feeling lost is totally normal.  
You don't really know what you're supposed to test, you're kinda poking around blindly… and if you're learning a test framework at the same time because you've never written a single test in your life… well, that's the perfect recipe for a few hours of confusion.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744401320826/ed6b1110-e865-4413-b912-7beb0394b702.png align="left")

But there are a few simple ideas that can really help.  
They won’t magically fix everything, but they’ll definitely help you get unstuck when you don’t know where to begin.

Because if you take two seconds to really think about it, **aside from edge cases like perf or security testing**, there are only **three ways** to check that a piece of code is doing what you expect.

<s>Not ten. Not fifty.</s> Just **three**.

Once you get that, everything starts to make a lot more sense.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In this article, we will discuss about output checking only.</div>
</div>

## The only question you need to ask

Start by asking yourself this one simple question:

> Is the result I want to verify **direct**, **indirect**, or part of a **collaboration**?

I can already see your face:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744401401150/16d1949a-013e-4361-8d00-8cb8e1702289.png align="left")

“Uhhh what? I have no idea what that means. Nevermind…”

→ Don’t worry. Let’s walk through it together with examples.

## 1\. Direct output

Let’s start with the simplest one, and the one you’ll try to use most often.  
It also happens to feel the most natural.

> Imagine the code you're writing is a "machine".  
> You don't really care what’s going on inside.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744401200549/a569b480-a9ee-4a35-a400-cc91a75dc7f1.png align="left")

> That machine has one purpose.  
> In our case: “**create a potion from a herb**”.

→ So the machine takes an `herb` as **input**...

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744401657523/65261c70-ddba-4535-929f-bad0ffe76ebd.png align="left")

→ ...and gives you a `potion` as **output**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744401702577/ec6e7534-3be7-4a16-ba2a-c51a37a9bda6.png align="left")

If we translate this into a TDD-style test, you might write something like:

```ts
it('should create a green potion from green herb', () => {
  // Arrange
  const machine = new Machine();
  const herb = 'green-herb';

  // Act
  const potion = machine.createPotion(herb);

  // Assert
  expect(potion).toBe('green-potion');
});
```

Pretty straightforward, right?

You feed a `herb` into the machine, and the **return value** is a green `potion`.

→ That’s a **direct** result.

Direct results are the easiest to test: you pass something in, you get something out, you check the output.

It’s the same with a simple calculation. Let’s say you're writing a `sum()` function:

```ts
it('should make the sum of two numbers', () => {
  // Arrange
  const a = 1;
  const b = 2;

  // Act
  const result = sum(a, b);

  // Assert
  expect(result).toBe(3);
});
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Sometimes, you don’t even need to pass anything as input, or maybe you just don’t have control over the inputs that easily, I know. But we’ll get to that in another article about direct and indirect inputs. For now, we’re focusing only on the <strong>output</strong>.</div>
</div>

In short: **if your code returns a value**, it’s **direct**, and that’s the easiest kind of thing to test.

## 2\. Indirect output

Sometimes things are a bit trickier.  
The result exists, but it’s visible **somewhere else**, and your test needs to account for that.

Let’s tweak our potion machine example just a bit.

> Our machine is connected to a lamp that turns green when the potion is ready.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744404470628/7e2d2d2e-9fae-4268-b9a9-b84cbb240e08.png align="left")

In this case, the `lamp` is **external** to the machine.  
The machine can somehow access it and update its state.

How? Who cares? What matters is that the machine **doesn’t return** the lamp.  
→ So this is an **indirect** result.

Here’s how that might look in a test:

```ts
it('should change the color of the lamp to green', () => {
  // Arrange
  const lamp = new Lamp();
  const machine = new Machine(lamp);
  const herb = 'green';

  // Act
  machine.createPotion(herb);

  // Assert
  expect(lamp.color).toBe('green');
});
```

You can clearly see from the test that the `machine` has access to the `lamp`, and after creating a `potion`, the lamp turns green.

It’s still readable and testable, but it’s a little more work than the direct case.

And that’s your **second way** of verifying results. One more to go.

## 3\. Collaboration

In some tests, what you want to verify isn’t a result or a state.  
It’s a **behavior**, something that happened.

You want to check whether the **right thing was done** by a collaborator.

Let’s go back to our potion machine:

> The lamp should blink 3 times to signal that a potion is ready.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744406064766/8c7f50ef-f28b-4e0f-85af-8db9e89d09ee.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1744406073692/a0aca34d-cf95-41aa-81ab-d27f8e356705.png align="left")

Now blinking the lamp could take time. It could drain power. Maybe those lightbulbs are expensive.

→ In tests, we want speed, low cost, and predictability.

So the better move is to use a **test double**: a fake object that helps us verify what happened.

We want to **verify** that our **collaborator** `lamp` had its `blink` method called exactly 3 times.

```ts
it('should make the lamp blink 3 times', () => {
  // Arrange
  const lamp = new Lamp();
  spyOn(lamp, 'blink').mockImplementation();    
  const machine = new Machine(lamp);
  const herb = 'green';

  // Act
  machine.createPotion(herb);

  // Assert
  expect(lamp.blink).toBeCalledTimes(3);
});
```

> ✋ Heads up! There are lots of ways to create test doubles.  
> Here I’m just sticking with what we’ve already discussed.  
> I used `spyOn` with `mockImplementation` (from Jest or Vitest) because you’ve probably seen that before in docs or tutorials.  
> But it’s **not** the only way!  
> In a future post, I’ll show how to create your own test doubles **without any library**, and break down the differences between **dummies**, **stubs**, **spies**, **fakes**, and **mocks**.

So here, you’re not checking a return value or a state.  
You’re checking that **another element** was **used correctly**.  
→ That’s what we call verifying a **collaboration**.

## TLDR:

Whenever you’re about to write a test, just ask:

* What does the code **produce**? → **Direct**
    
* What does it **change somewhere else**? → **Indirect**
    
* What does it **ask another object to do**? → **Collaboration**
    

## Want to go deeper?

Obviously, the names I use here are simplified, but this structure will help a ton when you're getting into TDD.

If you want to read more, here are the “official” names for these techniques:

* **Direct** → *State Verification*
    
* **Indirect** → *Observable Side Effect*, *Indirect Output*
    
* **Collaboration** → *Interaction Testing*, *Behavior Verification*
    

Some books if you want to dig in deeper (Amazon affiliate links):

* *xUnit Patterns* (Gerard Meszaros): [https://amzn.to/42pw3hZ](https://amzn.to/42pw3hZ) - Old one but great for testing patterns
    
* *TDD by Example* (Kent Beck): [https://amzn.to/3YvZu0G](https://amzn.to/3YvZu0G)
    

In an other article, we will discuss about direct and indirect inputs, stay tuned!
