Spy, Stub, Fake 그리고 Mock에 대해서 알아보자.
Tags: testdouble, java
Published: 2023-07-10 08:09:26

> [블로그](https://www.besfir.xyz/ko/category/dev/2023/find-out-spy-stub-fake-mock)에 작성한 글입니다. 코드에 라인 하이라이팅이 되지 않아서 글 만으로는 어색한 부분이 있을 수 있는데, 블로그에 오시면 확인이 쉽습니다.

## Test double

Test double(테스트 대역)은 영화 속 배우를 대신하는 Stunt double(스턴트 대역)에서 유래되었으며, **테스트를 진행하기 어려운 경우 이를 대신해 진행해 주는 객체를 뜻한다.** 다른 방식으로 설명하자면 테스트 대상에 집중할 수 있도록, 의존하는 구성요소를 테스트에 용이하도록 단순화한 객체를 뜻한다.

여기서 테스트 대상을 System Under Test 줄여서 SUT라고 부르고, SUT가 의존하는 구성요소를 Depended-On Component 줄여서 DOC라고 부른다. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfp71kltmxgwaxfee5ee.png)

SUT와 DOC 간의 협력이 어색하지 않다면, 해당 기능(행동)을 하나의 단위로 보고 함께 테스트 하는 것을 **Sociable test**라 한다. Sociable test의 경우, 현재 시간과 같이 SUT가 제어할 수 없는 상황이나 Email처럼 비용이 많이 드는 코드를 테스트 하기 어렵다.

이와 반대로, SUT와 DOC의 의존성을 끊고 하나의 모듈 단위로 테스트하는 것을 **Solitary test**라 한다. Solitary test의 경우 DOC를 단순하고 통제할 수 있는 객체로 만들어 SUT가 외부 어떠한 요인에도 영향을 받지 않도록 고립시켜 테스트 할 수 있다. 이때 만드는 DOC가 바로 테스트 대역이다.

> 💡 테스트 대역의 필요성에 대해서 서술하다보니 sociable test가 부정적으로 표현됐는데, 실제로는 그렇지 않다. 추후에 Solitary test와 Sociable test의 장단점을 정리해 보도록 하겠다. 

### 언제 사용하는가?

- **제어할 수 없는 상황을 통제할 때.**

랜덤 숫자, 현재 시간, DB에서 특정 레코드 읽기는 항상 같은 결과를 보장하지 않는다. 또한 네트워크 오류나 외부 서비스의 상태가 항상 정상이 아닐 수도 있다. 이처럼 SUT가 제어할 수 없는 상황을 일관된 상황으로 통제할 때 사용할 수 있다.

- **비용이 많이 드는 테스트를 할 때.**

이메일, SMS 또는 메신저 등으로 보내는 템플릿이나 메시지큐에 보내는 메시지 내용 등은 테스트 비용이 많이 들어 테스트하기 어려운 기능이다. 이 경우, 검증에 필요한 내용을 테스트 대역에 저장한 뒤 검증할 수 있다. 

- **테스트 작성을 위한 환경 구축할 때.**

특정 기능이 아직 구현되지 않아 테스트를 할 수 없는 경우 사용할 수 있다.  

## Test double의 종류

### Dummy  
가장 기본적인 테스트 대역으로, **파라미터 값으로 넘길때만 사용되며 실제로 쓰이지는 않는 객체**이다. 필드에 기본값만 채워져있는 객체나 메소드 구현부가 비어있는 객체가 이에 해당된다.
 
```java
@Test
void dummyTest() {
    Article dummyArticle = new Article("", "", new Date(), 0);
    
    assertThrows(Exception.class, () -> articleService.register(dummyArticle));
}
```
```java
class ConsolePrinter {
    public void print(String text){
        System.out.println(text);
    }
}

class DummyConsolePrinter extends ConsolePrinter {
    @Override
    public void print(String text) {}
}
```

### Stub  
Dummy에서 한 단계 업그레이드 된 객체이며 **하드 코딩된 값을 반환**한다.
테스트 중 발생하는 호출에 대해서 미리 결과를 준비해놓는 것은 Stubbing이라 한다.

```java
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)
        );
    }
}
```

### Spy  
실제 객체와 동일한 동작을 할 수도 있고 Stub으로써 동작할 수도 있다. 호출이 되었을 때, **테스트 검증에 필요한 데이터를 저장**한다.
 
```java
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);
    }
}
```

### Fake  
실제 객체의 **동작을 단순화해서 구현한 객체**이며, 동적으로 동작은 하지만 프로덕션 환경에서 사용하기는 적합하지 않다. HashMap을 이용해서 인메모리 DB를 구현한 것을 예로 들 수 있다.

```java
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;
    }

}
```

### Mock  

**Mock 라이브러리를 통해 동적으로 생성한 객체**이다. 구성하기에 따라 Dummy, Stub 또는 Spy처럼 동작한다.


> 💡 Mock 라이브러리 또는 테스트 프레임워크를 이용해서 Spy 객체 생성하면, **보통은 실제 객체처럼 동작하며 원하는 메소드를 선택적으로 Stubbing 해서 사용할 수 있다**. 필자가 관심을 두고 있는 Mockito, Mockk, Spock 그리고 Jest에서 확인했으나, 혹시 다른 경우가 있다면 제보 바란다.

```java
@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());

    }
}
```

## 테스트 대역의 한계

- **리팩토리 내성이 낮다.**

리팩토리 내성이 낮다는 말이 와닿지 않을 수 있는데, 관리 비용이 증가한다는 것과 같은 말이다. 예제를 통해 확인해보자. 테스트 대역을 이용하면 아래처럼 SUT의 내부 구현이 외부에 드러나게 된다.

```java
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();
}
```

이때 DOC의 메소드 이름이나 파라미터가 변경될 경우, 다시 말해 **메소드 시그니처가 변경될 경우 테스트 코드도 같이 수정되어야 한다**.

```java
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);
}
```

직접 만드는 테스트 대역의 경우에는 테스트 외부에서 Stubbing을 하기 때문에 중복이 줄어 그 정도가 덜하다. 그러나 Mock 라이브러리를 이용하는 경우, 테스트 내부에서 Stubbing을 하기 때문에 모든 테스트를 수정해야 하는 고통을 겪게 된다.

- **신뢰도가 떨어진다.**

Stubbing을 작성할 때, production에서 DOC가 실제 그렇게 동작하길 기대하면서 작성한다. 하지만 막상 실제 객체는 예상한대로 동작하지 않을 수 있다.

- **코드량이 지나치게 많아질 수 있다.**

_"리팩토리 내성이 낮다."_ 의 끝에서 언급한 이유로 코드량이 지나치게 많아질 수 있다. 

## 결론

각 테스트 대역을 구분하기 쉽게 최대한 간단하게 정리했지만, 실제 프로젝트를 진행하다보면 구분히 명확히 되지 않을 수 있다. 이는 사실 그리 이상한 일이 아닌데, MSDN 메거진에서는 아래의 그림을 예시로 들며 이론과 달리 실제로는 각 테스트 대역의 차이점이 모호하다고 얘기한다.

![테스트 대역의 스펙트럼](https://wbwsloxpaolxwerwiupa.supabase.co/storage/v1/object/public/blog-asset/images/6359cd30-db10-4487-bb72-fa8747daf75e.webp)

용어보다는 객체의 역할에 집중하는 것이 더 생산적일 것 같다.

## Reference

- [Mocks Aren't Stubs](http://martinfowler.com/articles/mocksArentStubs.html)
- [Exploring The Continuum Of Test Doubles](https://learn.microsoft.com/en-us/archive/msdn-magazine/2007/september/unit-testing-exploring-the-continuum-of-test-doubles)
- [What's the difference between a mock & stub?](https://stackoverflow.com/questions/3459287/whats-the-difference-between-a-mock-stub)
- [Stub: Mocking Object For Tests In Yuk Recycle](https://medium.com/ppl-a-4-yuk-recycle/stub-mocking-object-for-tests-in-yuk-recycle-7f4e1b6a75a9)
- [xUnit Patterns](http://xunitpatterns.com) 
- [Test Double](https://hudi.blog/test-double/)
- [Mock 객체 남용은 테스트 코드를 망친다.](https://medium.com/@chanhyeonglee/mock-%EA%B0%9D%EC%B2%B4-%EB%82%A8%EC%9A%A9%EC%9D%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%A7%9D%EC%B9%9C%EB%8B%A4-f38129e5d40a)