[EN] TDD: The 3 things you can test

EvywebEvyweb
5 min read

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.

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.

Not ten. Not fifty. Just three.

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

💡
In this article, we will discuss about output checking only.

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:

“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.

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

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

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

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

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:

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);
});
💡
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 output.

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.

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:

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.

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.

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:

  • DirectState Verification

  • IndirectObservable Side Effect, Indirect Output

  • CollaborationInteraction Testing, Behavior Verification

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

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

0
Subscribe to my newsletter

Read articles from Evyweb directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Evyweb
Evyweb