DEV Community

besfir1356
besfir1356

Posted on • Originally published at besfir.xyz

Let's find out about Spies, Stubs, Fakes, and Mocks.

This article is written on the blog. Line highlighting is not supported in the code block, so it may be awkward, but it's easy to check if you come to the blog.

Test double

Test double is derived from the stunt double that replaces the actor in the movie, and refers to an object that proceeds on behalf of the object when it is difficult to proceed with the test. In other words, it refers to an object that simplifies the components that SUT relies on so that it can focus only on the test target when testing.

Here, the test target is called SUT by shortening the System Under Test, and the component on which SUT depends is called DOC by shortening the Dependent-On Component.

Isolation under test in unit test

If the collaboration between SUT and DOC is not awkward, the function (behavior) is viewed as a unit and tested together is called a sociable test. In the case of Sociable test, it is difficult to test situations that SUT cannot control, such as the current time, or code expensive to test such as Email.

Conversely, breaking the dependence of SUT and DOC and testing them on a single module basis is called a solitary test. On the contrary, it is called solitary test to break the dependency of SUT and DOC, and test it in a single module unit. The DOC created at this time is the test band.

💡 To describing the need for the Test Double, the sociable test was expressed somewhat negatively, but in reality, it is not. Later, I will summarize the pros and cons of the Solitary test and Sociable test.

When is it used?

  • When you control an uncontrollable situation.

Random numbers, current time, and reading certain records from DB do not always guarantee the same results. In addition, network errors or external service conditions may not always be normal. In this way, it can be used to control situations that SUT cannot control as consistent situations.

  • When testing cost is expensive.

Templates sent by e-mail, SMS, or messenger, or message contents sent to message queues are difficult to test because of the high cost of testing. In this case, the contents necessary for verification may be stored in the Test Double and then verified.

  • When you build an environment for creating tests.

It can be used if a specific function is not yet implemented and testing is not possible.

Types of Test doubles

Dummy

It is the most basic test double, which is used only to pass over to the parameter value and is not actually used. This includes an object with only a default value filled in the field or an object with an empty method implementation unit.

@Test
void dummyTest() {
    Article dummyArticle = new Article("", "", new Date(), 0);

    assertThrows(Exception.class, () -> articleService.register(dummyArticle));
}
Enter fullscreen mode Exit fullscreen mode
class ConsolePrinter {
    public void print(String text){
        System.out.println(text);
    }
}

class DummyConsolePrinter extends ConsolePrinter {
    @Override
    public void print(String text) {}
}
Enter fullscreen mode Exit fullscreen mode

Stub

A step up from dummies, stubs are typically return hard-coded values.

class StubArticleRepository implements ArticleRepository {

    @Override
    public List<Article> getAll() {
        return List.of(
                new Article("title_1", "content_1", LocalDateTime.of(2020, 5, 19, 10, 50), 1),
                new Article("title_2", "content_1", LocalDateTime.of(2021, 8, 23, 10, 50), 0)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Spy

The same operation as the actual object may be performed or may be operated as a stub. When called, it stores the data needed for test verification.

class TestDoubleTest {

    @Test
    void spyTest() {
        SpyArticleRepository spy = new SpyArticleRepository();
        ArticleService service = new ArticleService(spy);
        Article stub1 = new Article("title_1", "content_1", LocalDateTime.of(2020, 5, 19, 10, 50), 1);
        Article stub2 = new Article("title_2", "content_2", LocalDateTime.of(2021, 6, 29, 11, 15), 2);

        Long id1 = service.register(stub1);
        Long id2 = service.register(stub2);

        assertEquals(1, id1);
        assertEquals(2, id2);
        assertEquals(2, spy.getArticles().size());      // Spy's save behavior happened 2 times

    }
}

class SpyArticleRepository implements ArticleRepository {

    private List<Article> articles = new ArrayList<>();

    @Override
    public Long save(Article article) {
        articles.add(article);
        return 1L;
    }

    public List<Article> getArticles() {
        return articles;
    }
}

class ArticleService {
    private ArticleRepository repository;

    public ArticleService(ArticleRepository repository) {
        this.repository = repository;
    }

    public Long register(Article article) {
        return repository.save(article);
    }
}
Enter fullscreen mode Exit fullscreen mode

Fake

It is an object implemented by simplifying the operation of an actual object, and although it is not hard-coded and operates dynamically, it is not suitable for use in a production environment. An example is an in-memory DB using HashMap.

class ArticleRepositoryFake implements ArticleRepository {

    private Map<Long, Article> articles = new HashMap<>();

    @Override
    public Long save(Article article) {
        Long existId = article.getId();
        if (existId == null) {
            return createNewArticle(article);
        }
        updateOldArticle(existId, article);
        return existId;
    }

    private void updateOldArticle(Long existId, Article article) {
        articles.put(existId, article);
    }

    private Long createNewArticle(Article article) {
        Long id = (long) (getAll().size() + 1);
        this.articles.put(id, article);
        return id;
    }

}
Enter fullscreen mode Exit fullscreen mode

Mock

It is an object created dynamically through the Mock library. Depending on the configuration, it works like Dummy, Stub, or Spy.

💡 When a Spy object is created using a Mock library or testing framework, it usually behaves like a real object and can optionally be used by stubbing the desired method. I've checked on Mockito, Mockk, Spock, and Jest that I'm interested in, but if there's anything else, please let me know.

@ExtendWith(MockitoExtension.class)
class TestDoubleTest {

    @Test
    @DisplayName("A mock behave like a dummy")
    void mockTest1(@Mock Member dummyMember) {
        Article article = new Article("Title 1", "Content 1", now(), 10, dummyMember);
        ArticleRepository repository = new ArticleRepository();

        assertDoesNotThrow(() -> repository.save(article));
    }

    @Test
    @DisplayName("A mock behave like a stub")
    void mockTest2(@Mock ArticleRepository stubRepository) {
        when(stubRepository.findById(anyLong())).thenReturn(new Article("Title 1", "Content 1", now(), 10, new Member("","")));
        ArticleService service = new ArticleService(stubRepository);

        Article article = service.fetch(1L);

        assertEquals("Title 1", article.getTitle());

    }

    @Spy
    ArticleRepository spyRepository;

    @Test
    @DisplayName("A mock behave like a spy")
    void mockTest3() {
        ArticleService service = new ArticleService(spyRepository);

        service.fetch(1L);
        service.fetch(2L);

        verify(spyRepository, times(2)).findById(anyLong());

    }
}
Enter fullscreen mode Exit fullscreen mode

Limit of test band

  • It has low resistance to refactoring.

Low resistance to refactoring is in line with increasing code management costs. Let's look at the example. When using the Test double, the internal implementation of the SUT is externally exposed as follows.

class SutServiceTest {

  @Test
  public void someMethodTest() {
    // given
    FooService mockFooService = mock(FooService.class);
    SutService sut = new SutService(mockFooService);

    // when
    when(mockFooService.bar()).thenReturn(1); // Internal implementation was exposed
    sut.someMethod();
    ...

    // then
    verify(mockFooService).bar();             // Internal implementation was exposed
    ...
  }
}

class SutService {
  private FooService fooService;

  public SutService(FooService fooService) {
    this.fooService = fooService;
  }

  public void someMethod() {
    fooService.bar();
  }
}

interface FooService{
  int bar();
}
Enter fullscreen mode Exit fullscreen mode

At this time, if the method name or parameter of the DOC changes, in other words, if the method signature changes, the test code must also be modified.

class RefactoringTest {

  @Test
  public void someMethodTest() {
    // given
    FooService mockFooService = mock(FooService.class);
    SutService sut = new SutService(mockFooService);

    // when
    when(mockFooService.bar()).thenReturn(1); // Break
    sut.someMethod();
    ...

    // then
    verify(mockFooService).bar();             // Break
    ...
  }
}

interface FooService{
  Bar bar(Factor factor);
}
Enter fullscreen mode Exit fullscreen mode

In the case of the Test double created manually, Since stubbing is performed outside the test, redundancy is reduced, so refactoring resistance is little less. However, if you use the Mock library, you suffer from having to modify all the tests because you stub inside the test.

  • The credibility is low.

When you write stubbing, you write it expecting the DOC to actually work in production like that. However, the real object may not work as expected.

  • The amount of code can be too much.

The amount of code can be too much for the reason mentioned at the end in "It has low resistance to refactoring."

Conclusion

Although each test double was summarized as simply as possible to make it easy to distinguish, it may not be clearly distinguished when working on the actual project. In fact, this is not unusual, and MSDN Magazine uses the following figure as an example and states that, unlike theory, the difference between each test double is actually ambiguous.

Spectrum of test double

I think I can't do anything else if I focus on terms, so I'm going to focus on the role of the object.

Reference

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more