Skip to main content

El Arte de la Guerra... del Testing: Dobles de tests

2025-03-14
Madrid, Spain
T3chFest 2025
Clean CodeTestingJava

On March 14, 2025, I had the opportunity to give a complete talk at T3chFest on test doubles, and how they can help us improve the quality of our code. It was an incredible experience to share knowledge with the technical community at one of the most important events in Spain.

Below I share the content I discussed in the talk.

Introduction

Have you ever lost hours debugging a test that was failing randomly? Or have you seen how a simple change in a dependency breaks multiple tests? If you've been through this, you know how frustrating it can be.

Good morning everyone, my name is Aitor Santana and I come from the sunny Gran Canaria. I'm passionate about video games, a hobby that has been with me since childhood. However, my professional career has taken a different but equally exciting path: web development. I currently work on the backend with C# and the .NET ecosystem at Lean Mind. Although I don't develop video games, the problem-solving and creativity I've developed through gaming have been very useful in my career. I'm here to share my experience in the fascinating world of software testing.

Today I want to help you understand how test doubles can make our tests more reliable and maintainable. But I also want to talk about the problems they can cause if we don't use them correctly.

Context

As you go deeper into the basics of testing, you inevitably hit a stumbling block: mocks. Understanding these structures can be a significant challenge, as there are several types that adapt to different use cases.

As we can see in data available online, most developers find mocks moderately or highly difficult. This is due to several factors:

  • Practical Application: Knowing that there are several types of mocks and understanding when to use each one is crucial, and it's one of the objectives of this talk.

  • Context Identification: Related to the previous point, it's essential to know the available types of mocks and how to apply them according to your project's needs.

  • Software Design Principles: The compatibility of mocks with your project depends largely on the type of architecture followed and the software design principles adopted.

One aspect I particularly highlight is how different testing frameworks define the concept of mock. This variability can generate confusion, which requires mastering the art of war... or rather, of testing, to navigate these challenges effectively.

To facilitate this process and improve understanding of mocks, I'll base myself on Gerard Meszaros' terminology, who makes a clear distinction of the types of test doubles according to their use case. I'll also draw on a diagram proposed by Robert C. Martin in his book "Clean Craftsmanship: Disciplines, Standards, and Ethics", which was of great help to me in fully understanding these concepts.

TDD and Test Doubles

Test Driven Development is a methodology with 3 simple steps:

  1. First write the test, see it go red

  2. Write the production code, make it go green

  3. Clean up the code, refactor it

Test doubles are quite useful if you do TDD, especially Outside-In, when you want to test from the outermost part of your system and you haven't yet developed the code for the collaborators.

It should be noted that using test doubles doesn't require applying TDD. For example, in legacy projects where you already have code but want to add tests, you'll very likely need to use test doubles.

Test Desiderata

The term "Test Desiderata" refers to a set of principles or desirable characteristics for software tests. We highlight two fundamental principles:

  • Determinism: Test doubles help eliminate randomness and dependence on external factors (such as databases, APIs, or external services). This makes tests predictable and reliable.

  • Isolated (Micro): The use of test doubles helps tests be smaller and more specific, avoiding external dependencies and ensuring each unit of code is tested in isolation.

Social vs Solitary Tests

  • A social test is one where the behavior of an artifact is tested using its real dependencies, with all the implications this can have. It's a higher-level test.

  • A solitary test is one where the behavior of an artifact is tested using test doubles of its dependencies, so it will have a more isolated, safe and less costly environment.

This doesn't mean all tests should be solitary. Sometimes you'll want to test with real dependencies to see the complete flow of business rules.

Types of Test Doubles

Dummy

A dummy is a type of test double used when you need to pass an object to a component under test but the behavior of the double isn't relevant to the test.

java
public class AuthenticatorDummy implements Authenticator {
  @Override
  public boolean authenticate(String username, String password) {
    return false;
  }
}

@Test
void when_closed_login_is_canceled() {
  Authenticator authenticator = new AuthenticatorDummy();
  LoginDialog dialog = new LoginDialog(authenticator);

  dialog.show();
  dialog.close();

  assertFalse(dialog.isOpen());
}

Stub

A stub provides predefined responses to calls made to it during the test — they have state or memory.

java
public class AuthenticatorStub implements Authenticator {
  private final boolean allowLogin;

  public AuthenticatorStub(boolean allowLogin) {
    this.allowLogin = allowLogin;
  }

  @Override
  public boolean authenticate(String username, String password) {
    return allowLogin;
  }
}

Spy

A Spy records information about how it's used during tests. In addition to providing predefined responses like a Stub, it also allows you to ask questions about its usage.

java
public class AuthenticatorSpy implements Authenticator {
  private final boolean allowLogin;
  private int calls = 0;
  private String registeredUserName;
  private String registeredPassword;

  @Override
  public boolean authenticate(String username, String password) {
    calls++;
    registeredUserName = username;
    registeredPassword = password;
    return allowLogin;
  }

  public int calls() { return calls; }
  public String registeredUserName() { return registeredUserName; }
  public String registeredPassword() { return registeredPassword; }
}

Strict Mock

A Strict Mock not only simulates the behavior of an object but also verifies that expected calls are made to its methods with specific parameters.

Main problems:

  • Test fragility

  • Refactoring difficulty

  • Focus on implementation rather than behavior

  • High maintenance cost

Fake

It's an object that simulates the real behavior of the artifact by implementing some business rules in a rudimentary or simplified way.

java
public class AuthenticatorFake implements Authenticator {
  @Override
  public boolean authenticate(String username, String password) {
    return username.equals("username") && password.equals("good password");
  }
}

Additional Patterns

Test Specific SubClass

This pattern is used when we want to test a specific method, but we know this method can make calls to other methods that produce side effects. We create a specific subclass and override the problematic method.

Shelf Shunt

This is a variation of the Test-Specific Subclass pattern, but in this case the test class itself overrides the unwanted behavior.

Humble Object

Used to separate hard-to-test code from easy-to-test code. We delegate that hard-to-test code to another class so small that we don't need to test it directly.

Resources

I'd like to leave you with some resources:

You can watch the full talk on YouTube, and the code examples in the repository.