테스트에 대한 다양한 고찰

주노·2023년 5월 9일
0

기술부채 알쓸신잡

목록 보기
8/22
post-thumbnail

서론

단위테스트는 무엇일까? 라는 고민거리가 있었다.

어떤 객체에 대해 단위테스트를 작성하려는데 협력객체가 존재한다면 이는 단위테스트라고 할 수 있을까???

브라운이 설명해준 테스트에 대한 고찰 내용을 정리해보자.

단위테스트

단위테스트는 다음과 같은 특징이 있다.

  • 작은 코드조각을 검증한다
  • 빠르게 수행 가능하다
  • 격리된 방식으로 처리가 가능해야한다.

다음과 같은 상황을 생각해보자.

Line line = new Line(new Station(), new Station);

Line이라는 객체를 테스트하기 위해 Line과 협력하고 있는 객체인 Station이라는 객체도 함께 사용해야한다.

협력객체가 같이 존재하는데 이걸 정말 단위테스트라고 할 수 있을까?

만약 Station에 대한 변경이 있으면 Line에 대한 테스트도 변경되고, 위에서 이야기한 작은 코드조각이라는 정의가 조금 빗나간다는 느낌이 들기도 한다.

그렇다면 협력하는 객체에 대해서는 단위테스트를 어떻게 수행할 수 있을까?

다음과 같은 두 가지 방법을 생각해 볼 수 있다.

통합 단위테스트

통합 단위테스트 : 실제 객체를 사용하면서 구성한다.

@BeforeEach
setUp() {
	Line line = new Line(new Station(), new Station);
}

고립 단위테스트

고립 단위테스트 : Mock과 같은 가짜객체를 만들어서 조건에 맞춰서 기능이 동작하도록 구성한다.

@BeforeEach
setUp() {
	Line line = mock(Line.class);
}

테스트 더블

mock()이라는 코드를 보자.

우리는 가짜 객체를 만들어서 테스트를 수행할 수 있다.

이 때 가짜 객체를 구현하는 방법은 여러가지가 있다.

Mock, Stub, Fake , Dummy 등등..

Stub 방식

stub : (나무의) 그루터기(stump); (넘어진 나무 등의) 뿌리, 잘린 나머지

Stub은 어떤 기능을 대리하여 수행하는 코드라고 생각하면 된다.

우리는 협력 객체를 Stub으로 둘 수 있다.

실제로는 어떤 동작이 일어나는지 모르지만 어떤 동작을 했을 때 해당 결과를 임의로 정의할 수 있다.

대표적인 방식으로 아래와 같이 Mockito를 사용하는 방식을 확인할 수 있다.

@DisplayName("단위 테스트 - mockito를 활용한 가짜 협력 객체 사용")  
public class MockitoTest {  
@Test  
void findAllLines() {  
	// given  
	LineRepository lineRepository = mock(LineRepository.class);  
	StationRepository stationRepository = mock(StationRepository.class);  
	when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));  
	LineService lineService = new LineService(lineRepository, stationRepository);  
	// when  
	List<LineResponse> responses = lineService.findAl1Lines();  
	// then  
	assertThat(responses).hasSize(1);  
	}  
}

Fake 방식

Fake 객체는 실제로 사용된 객체는 아니지만 같은 동작을 하도록 구현된 객체다.

전략패턴이 대표적인 예시다.

자동차 경주코드를 예시로 가져와보자.

private static class AlwaysMoveGenerator implements RandomNumberGenerator {
	@Override  
	public int generate() {  
		return 4;  
	}  
}

RandomNumberGenerator라는 객체는 무작위 번호를 생성하는 역할을 한다.

우리는 특정 동작을 수행하도록 임의의 Fake 객체를 만들 수 있다.

Dummy 방식

객체가 필요하지만 내부 기능은 필요하지 않을 때 사용할 수 있는 방식이다.

Spy 방식

Spy방식은 Stub의 역할과 동일하지만 정보를 더 기록한다.

예를 들면 어떤 Stub이 몇 번 호출 되었는지 기록하는 과정이 추가된다면 Spy 객체라고 볼 수 있겠다.

통합과 고립

협력 객체가 존재할 때 테스트코드를 작성한다면 다음과 같은 고민을 할 수 있다.

  1. 가짜 객체를 만든다 (고립)
  2. 실제 객체로 테스트를 수행한다 (통합)

고립의 경우 / Mockist

  • 가짜 객체를 활용하면 실제 객체를 사용할 때 보다 편하게 테스트를 작성할 수 있다.
  • 가짜 객체를 만드는 과정에서 상세 구현에 의존하는 테스트가 될 수 있다는 단점이 있다

통합의 경우 / Classist

  • 실제 객체에 의존하고 있어야한다.

Mockist TDD vs Classical TDD

Mockist TDD : 내부 구현이 되어있지 않은 상태에서 TDD를 하기 때문에 Out-In 방향으로 행위를 기반으로 TDD를 진행하는 방식이다.

Classical TDD : 실제 객체를 사용하여 TDD를 진행하기 때문에 최하위 도메인부터 구현하는 In - Out 방식을 사용한다.

Classical TDD는 다음과 같은 특징이 있다.

  • 도메인 설계가 충분히 이루어진 다음에 진행해야한다.
  • TDD 사이클을 이어나가기가 Mockist TDD에 비해 상대적으로 어렵다.

반대로 Mockist TDD는 다음과 같은 특징이 있다.

  • 요구사항에 대한 행위를 먼저 정의하고, 가짜 객체로 행위를 구현하며 진행한다.
  • 행위를 먼저 정의하고, 협력객체가 가짜로 구현되어있기 때문에 TDD 사이클을 이어나가기 수월한 구조다.

그래서 뭘 사용하는데?

아는것에서 모르는 것으로 known to unknonw 방향을 잡는것이 좋다.

도메인 이해도가 높다면 Classical TDD가 조금 더 적합해보이고
그렇지 않다면 요구사항 및 기능/행위를 기반으로 Out-In 방식을 적용하면서 Mockist TDD를 사용해볼 만 하다.

추천 도서 : 단위테스트

추천 사이트 : 마틴파울러 블로그

Reference

https://codinghack.tistory.com/92

https://martinfowler.com/bliki/UnitTest.html

브라운 🌸🐻🌸

profile
안녕하세요 😆

0개의 댓글