1. 비즈니스 로직을 테스트하기 전에 알면 좋은 지식
1-1. Layered Architecture와 테스트
이 글을 보기 전에 아래 글을 먼저 보고 오는 것을 추천한다. 1-1.Layered Architecture와 1-2.테스트의 분류만 읽고 와도 충분하다.
https://myvelop.tistory.com/223
간단히 요약하자면, Business Layer는 비즈니스 로직을 수행하는 계층으로 Business Layer 테스트는 로직이 잘 수행되는지를 중점에 두고 수행하면 된다.
1-2. BDD
BDD란 Behavior-Driven Development의 약자로 행위 주도 개발을 의미한다.
테스트할 때 상태의 변화에 집중하며 Given, When, Then으로 구조를 가진 시나리오를 만들어 테스트하는 것을 권장한다. 테스트 케이스의 시나리오는 개발자가 아닌 사람이 봐도 이해할 수 있을 정도로 만드는 것이 좋다.
- Feature : 테스트에 대상의 기능/책임을 명시
- Scenario : 테스트 목적에 대한 상황을 설명
- Given: 시나리오 진행에 필요한 값을 설정
- When: 시나리오를 진행하는데 필요한 조건을 명시
- Then: 시나리오가 끝났을 때의 상태 변화(결과)를 명시
class OrderService {
@Test
@DisplayName("주문")
void order() { // Feature & Scenario
// given
// when
// then
}
}
1-3. Classicist vs Mockist
단위테스트의 격리(Solitary)는 무엇보다 중요하기 때문에 연관된 모든 객체에 Test Double을 사용해야 한다고 주장하는 사람들을 Mockist라고 부른다. 반면, 최대한 실제 객체를 사용하되 사용이 어려운 경우에만 Test Double을 사용해 협동(Sociable) 테스트를 만드는 것이 좋다고 생각하는 사람들을 Classicist라고 한다.
예를 들어 OrderService가 PointService, PaymentService에 의존하는 코드의 테스트를 작성한다고 가정해보자.
public class OrderService {
private final OrderRepository orderRepository;
private final PointService pointService;
private final PaymentService paymentService;
public Order order(OrderRequest request) {
Point point = pointService.usePoint(request);
Payment payment = paymentService.saveOf(request);
return orderRepository.save(Order.of(request, point, payment));
}
}
아래는 Classicist의 코드 작성 예시이다. OrderService와 의존관계인 PointService와 PaymentService의 객체는 그대로 사용하고 그대로 사용하기 어려운 Repository만 Fake로 구현해 테스트를 만들었다.
class OrderService {
OrderService orderService;
FakeOrderRepository orderRepository;
PointService pointService;
FakePointRepository pointRepository;
PaymentService paymentService;
FakePaymentRepository paymentRepository;
@BeforeEach
void setUp() {
orderRepository = new FakeOrderRepository();
pointRepository = new FakePointRepository();
paymentRepository = new FakePaymentRepository();
pointService = new PointService(pointRepository);
paymentService = new PaymentService(paymentRepository);
orderService = new OrderService(orderRepository, pointService, paymentService);
}
// 테스트 작성
}
반면 Mockist라면 아래와 같이 코드를 만들 것이다. 테스트 과정에서 발생하는 요청의 모든 반환 값은 Test Double을 통해 만든다.
@extendwith(mockitoextension.class)
class OrderService {
@InjectMocks
OrderService orderService;
@Mock
PointService pointService;
@Mock
PaymentService paymentService;
// 테스트 작성
}
위의 2가지 방법은 정답은 없다. 본인이 맞다고 생각하는 방식을 선택해 테스트를 작성하면 된다. (cf. 마틴 파울러 옹은 2가지 방식 모두 존중한다고 했고, 본인은 classicist라고 함.)
2. Test Double
Test Double. 테스트 대역. 테스트하려고 하는 객체와 의존관계가 있는 객체의 모조품을 만들어 대역을 세우는 것을 의미한다. 보통 Test Double은 아래와 같은 경우에 사용된다.
- 테스트하려는 객체를 격리하여 테스트하고 싶은 경우
- 테스트가 어려운 외부 API를 다른 것으로 대체하고 싶은 경우
테스트 대역의 종류에는 Dummy, Fake, Stub, Mock, Spy 등이 있다.
2-1. Dummy
가장 간단한 방법이다. Test Double로 작성될 객체의 내부 기능이 필요하지 않을 때 사용하는데, 보통 void 반환값을 지닌 함수를 텅빈 함수로 만들어 사용하게 한다. 서비스가 인터페이스를 의존하고 있어야 사용할 수 있다.
아래는 메일을 전송하는 객체를 Dummy로 만든 예시이다.
public interface MailSender {
void sendMail(String subject, String content);
}
public class DummyMailSender implements MailSender {
@Override
public void sendMail(String subject, String content) {}
}
2-2. Fake
Dummy와는 다르게 실제 동작하는 코드를 가지고 있는 Test Double이다. 실제 운영환경처럼 정교하게 동작하지는 않지만 흉내내는 정도로 만들어 사용한다. Dummy와 마찬가지로 서비스가 인터페이스를 의존하고 있어야 사용할 수 있다.
public interface OrderRepository {
Order save(Order order);
}
public class FakeOrderRepository implements OrderRepository {
public long autoIncrement = 1;
public List<Order> orders = new ArrayList<>();
@Override
public Order save(Order order) {
ReflectionTestUtils.setFiled(order, "id", autoIncrement++);
this.orders.add(order);
return order;
}
}
2-3. Stub
미리 준비된 반환값을 전달하는 객체이다. Dummy와 Fake의 중간이라고 생각하면 된다.
public class StubOrderRepository implements OrderRepository {
@Override
public Order save(Order order) {
return new Order(....);
}
}
2-4. Mock
위에 예시로 보여준 것들과는 다르게 서비스가 인터페이스를 의존하지 않고 있더라도 사용할 수 있다. Mockito 객체를 사용해 손쉽게 모킹을 사용할 수 있다. 어노테이션을 사용해 모킹 객체를 만드는 방식은 뒤에서 설명하도록 하겠다.
@Service
@RequiredConstructor
public class OrderService {
private final OrderRepository orderRepository;
public Order order(OrderRequest request) {
Order order = Order.of(request);
return orderRepository.save(order);
}
}
public class OrderServiceTest {
OrderService orderService;
OrderRepository orderRepository;
@Test
@DisplayName("주문 요청을 받아 주문을 저장할 수 있다.")
void order() {
// given
orderRepository = Mockito.mock(OrderRepository.class);
orderService = new OrderService(orderRepository);
OrderRequest request = createOrderRequest();
Order returnOrder = createOrder(1L);
Mockito.when(orderRepository.save(any())).thenReturn(order);
// when
Order order = orderService.order(request);
// then
assertThat(result.getId()).isEqualTo(1L);
}
private OrderRequest createOrderRequest() {
return OrderRequest.builder()
...
.build();
}
private Order createOrder(final Long id) {
return Order.builder()
...
.build();
}
}
대충 보면 Mock과 Stub이 다를 게 없어보이지만 엄연히 다른 개념이다. Stub은 상태 검증(State Verification)을 하기 위한 수단으로 어떤 기능을 요청했을 때 내부적인 상태가 어떻게 바뀌었는지에 집중한다. 반면 Mock은 행위 검증(Behavior Verification)에 사용되는데, 어떤 메소드가 실행했을 때 어떤 결과가 Return되는지가 중요하다.
2-5. Spy
Stub의 역할을 하면서 호출될 때마다 기록을 하는 객체이다. 특정 메소드가 호출되었는지 여부를 확인할 수 있는 Test Double이다.
public class SpyMailSender implements MailSender {
private int sendMailCallCount = 0;
@Override
public void sendMail(String subject, String content) {
callSendMailCount++;
}
public int getSendMailCallCount() {
return this.sendMailCallCount;
}
}
3. Test Double을 사용해 단위테스트 작성해보기
이제 Test Double을 사용해 단위테스트(소형테스트)를 작성하는 방법에 대해 살펴보자.
3-1. @ExtendWith로 더 쉬운 Mock 만들기
위에서 Mockito를 사용해 간단하게 Mock 객체 만드는 방법을 알아봤다. 확장 모듈을 가져오는 @ExtendWith을 사용하면 Mockito를 간편하게 사용할 수 있다. @ExtendWith(MockitoExtension.class) 라고 테스트 상단에 선언하면 테스트를 실행할 때 Mockito 확장 모듈을 가져와 사용할 수 있다.
- 이전에는 Mockito.mock이라는 메소드로 정의 사용했던 Mock 객체를 아래와 같이 @Mock 하나로 만들 수 있다.
- @InjectMocks 어노테이션을 사용해 자동으로 테스트 대상이 될 객체를 생성하고 @Mock으로 생성된 객체를 자동으로 주입받게 만들 수 있다.
@Mock으로 생성된 객체는 Mockito.when() 메소드를 사용해 모킹할 수 있다. 특정 함수가 호출됐을 때 반환할 값을 지정해주면 테스트가 실행될 때
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@InjectMocks // 생성된 Mock 객체를 자동으로 주입받아 생성
OrderService orderService;
@Mock // Mock 객체 자동 생성
OrderRepository orderRepository;
@Test
@DisplayName("주문 요청을 받아 주문을 저장할 수 있다.")
void order() {
// given
OrderRequest request = createOrderRequest();
Order returnOrder = createOrder(1L);
Mockito.when(orderRepository.save(any())).thenReturn(order);
// when
Order result = orderService.order(request);
// then
assertThat(result.getId()).isEqualTo(1L);
}
private OrderRequest createOrderRequest() {
return OrderRequest.builder()
...
.build();
}
private Order createOrder(final Long id) {
return Order.builder()
.id(id)
...
.build();
}
}
3-2. FakeRepository
Service에서 주입받을 Repository를 한 번쯤 Fake로 구현해볼 것을 권장한다. 다른 Repository를 Fake로 만들면 다른 Test Double을 사용하는 것보다 더 장점이 많다고 생각한다. 내가 생각하는 FakeRepository의 강점은 아래와 같다.
- 프로젝션을 반환하는 메소드 요청이 있는 테스트 케이스에서의 연관 관계 설정을 강제할 수 있다. (문서로서의 테스트! 자세한 것은 예시에서 설명) Mocking의 경우 프로젝션 반환 값만 작성하면 되기 때문에 테스트 케이스의 문맥 파악이 제대로 되지 않는 경우가 발생할 수 있다. (대신 Fake를 사용할 시 테스트 작성의 난이도가 올라가고 유지보수를 지속적으로 해줘야 한다는 단점이 생길 수 있다.)
- Stub으로 만들 경우 정해진 반환 값만 받을 수 있지만, Fake는 테스트 케이스마다 다른 값을 넣어 조건에 따라 테스트하기 유리하다. 따라서 실제 환경과 같은 테스트를 구성하는데 도움이 될 수 있다.
구조
Service는 Repository 구현체를 의존하지 않고 인터페이스를 의존한다. (의존성 역전) 운영에서 사용할 구현체와 FakeRepository 구현체는 Repository Interface를 구현하며, 실제 운영환경에서는 빈으로 등록된 구현체를 서비스에서 주입받게 될 것이다.
반면, 단위테스트에서는 Test Double을 사용하기 위해 Service를 생성할 때 FakeRepository를 주입해준다.
코드 예시
아래와 같이 여러 도메인 객체와 연관된 객체 OrderLine과 이 객체를 사용하기 위한 레포지토리, 서비스가 있다.
public class OrderLine {
private Long id;
private Order order; // N:1 관계
private Item item; // N:1 관계
private List<OrderLineCompose> composes = new ArrayList<>(); // 1:N 관계
private long price;
...
}
public interface OrderLineRepository {
OrderLine save(OrderLine orderLine);
Optional<OrderLine> findById(long id);
List<OrderLineProjection> findByOrderNumber(String orderNumber);
}
public class OrderLineService {
private final OrderLineRepository orderLineRepository;
pubilc OrderLineService(OrderLineRepository orderLineRepository) {
this.orderLineRepository = orderLineRepository;
}
public OrderLine order(final OrderRequest request) {
OrderLine orderLine = OrderLine.of(request);
return orderLineRepository.save(orderLine);
}
public Optional<OrderLine> findById(final Long id) {
return orderLineRepository.findById(id);
}
public List<OrderLineProjection> findByOrderNumber(final String orderNumber) {
return orderLineRepository.findByOrderNumber(orderNumber);
}
}
Fake 레포지토리는 OrderLineRepository Interface를 구현했고, 실제 레포지토리와 비슷하게 동작하도록 로직을 작성했다. List 자료구조로 데이터도 저장하고 조건에 따라 값을 반환할 수 있다.
save() 메소드로 값을 저장할 때 리플렉션을 활용했다. private으로 지정되어 있는 id에 autoIncrement 값을 넣어주기 위해 ReflectionTestUtils.setField() 메소드를 사용했다. (실제 JPA 환경에서도 Entity 필드에 값을 넣기 위해 리플렉션을 사용)
public class FakeOrderLineRepository implements OrderLineRepository {
public long autoIncrement = 1;
public List<OrderLine> data = new ArrayList<>();
@Override
public OrderLine save(final OrderLine orderLine) {
ReflectionTestUtils.setField(orderLine, "id", autoIncrement++);
this.data.add(orderLine);
return orderLine;
}
@Override
public Optional<OrderLine> findById(final Long id) {
return data.stream()
.filter(orderLine -> id.equals(orderLine.getId()))
.findAny();
}
@Override
public List<OrderLineProjection> findByOrderNumber(final String orderNumber) {...}
}
프로젝션을 사용한다면 아래와 같이 설정된 관계들을 가져와 값을 넣어주자. Mocking을 했다면 프로젝션 그 자체를 작성했을 것이고 그대로 반환받았겠지만 Fake를 사용하면 테스트 케이스에서 관계 설정을 해주는 것이 강제되기 때문에 해당 케이스의 상황을 파악하기 더 수월해지고 이는 문서로서의 테스트를 작성하기 위한 길이 될 수 있다고 생각한다.
public class OrderLineProjection {
private final String orderNumber;
private final Long orderLineId;
private final String thumbnail;
private final String itemName;
private final Integer quantity;
private final Long price;
// constructor
...
// getter
...
}
public class FakeOrderLineRepository implements OrderLineRepository {
private long autoIncrement = 1;
public List<OrderLine> data = new ArrayList<>();
@Override
public OrderLine save(final OrderLine orderLine) {...}
@Override
public Optional<OrderLine> findById(final Long id) {...}
@Override
public List<OrderLineProjection> findByOrderNo(final String orderNumber) {
return data.stream()
.filter(
orderLine ->
orderNumber.equals(orderLine.getOrder().getOrderNumber()))
.map(
orderLine ->
new OrderLineProjection(
orderLine.getOrder().getOrderNumber(),
orderLine.getId(),
orderLine.getItem().getThumbnail(),
orderLine.getItem().getName(),
orderLine.getOrderLineComposes().stream()
.mapToInt(OrderLineCompose::getQuantity)
.sum(),
orderLine.getPrice()))
.collect(Collectors.toList());
}
}
이렇게 구현한 FakeRepository를 서비스 테스트 코드에 주입해 사용하면 된다.
class OrderLineServiceTest {
OrderLineService orderLineService;
FakeOrderLineRepository orderLineRepository;
@BeforeEach
void setUp() {
this.orderLineRepository = new FakeOrderLineRepository();
this.orderLineService = new OrderLineService(this.orderLineRepository);
}
@Test
@DisplayName("주문번호로 주문 아이템 목록을 조회할 수 있다.")
void findByOrderNumber() {
// given
Item item = createItem();
Order order = createOrder();
OrderLineCompose compose = createOrderLineCompose();
OrderLine orderLine = createOrderLine(item, order, List.of(compose));
orderLineRepository.save(orderLine);
// when
OrderLineProjection result = orderLineService.findByOrderNumber("주문번호");
// then
assertThat(result.getOrderNumber()).isEqualTo("주문번호");
}
private Item createItem() {...}
private Order createOrder() {...}
private OrderLineCompose createOrderLineCompose() {...}
private OrderLine createOrderLine(
final Item, final Order order, final List<OrderLineCompose> composes) {
return OrderLine.builder()
.item(item)
.order(order)
.composes(composes)
...
.build();
}
}
4. 읽기 쉽고 효율적인 단위테스트를 만들기 위해
4-1. Mockito보다는 BDDMockito
Mockito.when()을 보면 BDD스럽지 않다는 느낌을 받을 수 있다. BDD에 따르면 반환값을 설정하는 단계인 Mockito.when()은 Given절에 들어 가는 것이 마땅한데, when이라는 메소드명을 가지고 있기 때문이다. Mockito를 BDD스럽게 만든 BDDMockito를 사용하면 이런 문제를 해결할 수 있다.
@SuppressWarnings("unchecked")
public class BDDMockito extends Mockito {
...
}
BDDMockito는 Mockito를 상속받아 만든 클래스로 사용방법은 Mockito와 거의 동일하다. 대신 BDD의 시나리오에 맞게 테스트 코드를 볼 수 있도록 메소드 명만 변경했다. 아래의 예시를 살펴보자. when() 메소드가 given()으로 네이밍이 바뀌었고, 체이닝 메소드인 thenReturn()에서 willReturn()로 변경된 것을 확인할 수 있다.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@InjectMocks
OrderService orderService;
@Mock
OrderRepository orderRepository;
@Test
@DisplayName("주문 요청을 받아 주문을 저장할 수 있다.")
void order() {
//given
OrderRequest request = createOrderRequest();
Order returnOrder = createOrder(1L);
// BDDMockito의 given 메소드를 사용 가독성 좋은 테스트 코드를 만들 수 있다.
BDDMockito.given(orderRepository.save(any())).willReturn(returnOrder);
//when
Order order = orderService.order(request);
//then
assertThat(order.getId()).isEqualTo(1L);
}
private OrderRequest createOrderRequest() {
return new OrderRequest(...);
}
private Order createOrder(final Long id) {
return Order.builder()
.id(id)
...
.build();
}
}
4-2. Nested Class를 적절하게 섞자.
하나의 Feature에서 여러 개의 케이스가 존재할 때 @Nested 어노테이션을 사용해 엮어서 보여주는 것이 더 깔끔해보일 수 있다. Nested Class는 BDD의 Feature, Scenario의 구분에도 도움이 된다.
class OrderServiceTest {
...
@Nested
@DisplayName("주문할 때, ")
class order { // Feature 단위
@Test
@DisplayName("case1이 발생할 수 있다.")
void case1() {...} // Scenario1
@Test
@DisplayName("case2가 발생할 수 있다.")
void case2() {...} // Scenario2
}
}
4-3. 테스트는 빠르게
당연하게도 단위테스트는 속도가 가장 중요하다고 생각한다. (좋은 테스트는 FIRST 원칙을 따른다고 하는데, 이 중 F(Fast)가 빠른 속도를 의미하는 원칙) 따라서, 통합테스트보다는 단위테스트 위주로 테스트를 작성하는 것이 좋다고 생각한다.
가장 지양해야 할 테스트 코드는 @SpringBootTest를 달아놓고 @MockBean을 사용해 의존관계를 전부 모킹해 단위테스트를 만드는 것이다. 단위테스트는 가장 가벼운 방식으로 테스트해야 한다는 걸 명심하자.
4-4. 모든 클래스의 테스트 코드를 작성
"클래스마다 테스트 코드를 갖춰야 한다.”
David A. Thomas - 리팩토링, 마틴 파울러 -
(만약 도메인 주도 설계를 했다면) 비즈니스 로직은 서비스 계층에만 한정되지 않는다. 오히려 도메인 계층에 중요한 비즈니스 로직이 담겨 있는 경우가 더 많을 것이다. (서비스 계층은 도메인 객체에게 일을 시키는 동작만 하도록 기능을 한정하는 게 좋다.)
서비스 계층 테스트만 작성했을 때 도메인 객체에 결함이 있음에도 불구하고 얼렁뚱땅(?) 테스트가 잘 통과하는 경우가 생길 수 있다. 각각의 도메인 객체의 테스트를 생성해 단위테스트를 만들자. 도메인 단위테스트는 격리된 테스트를 작성하기 가장 좋은 단위이다. 각 도메인이 요구사항에 맞게 잘 동작하는지 확인하는 것은 무엇보다 중요하다.
다른 계층의 테스트에서도 통용되는 법칙이다. Presentation Layer와 Persistence Layer의 DTO에 변환 로직이 있다면 그 로직에 대해서도 테스트를 작성해주는 것이 좋다고 생각한다.
public record OrderSearchParam(...) { // from Controller DTO
public OrderQueryCondition toCondition() {
return new OrderQueryCondition(...) // to Repository DTO
}
}
4-5. 중요한 것은 테스트 커버리지가 아니다. (feat. 경계값 테스트)
커버리지가 100% 채워졌다는 사실이 그 코드가 완벽하다는 것을 보장하진 않는다. 우리가 테스트를 작성할 때 고려해야할 것은 커버리지가 아니라 로직이 얼마나 빈틈 없이 테스트되었는지다. 요구사항과 그에 따른 예외 상황을 모두 정리해놓고 테스트 코드에 그대로 작성하자.
여기서 경계값 테스트를 활용하면 좋다. 경계값 테스트란 입력 값이 특정 범위의 경계에 위치할 때 프로그램이 잘 동작하는지 확인할 때 사용하는 테스트이다.
class OrderTest {
@Nest
@DisplayName("주문할 때, ")
class order {
@Test
@DisplayName("10000원 이상 주문하면 100원이 할인된다.")
void case1() {
// given
OrderRequest request = createOrderRequest(15_000);
// when
Order order = Order.order(request);
// then
assertThat(order.getDiscountAmount()).isEqualTo(100);
}
@Test
@DisplayName("10000원 주문하면 100원이 할인된다.")
void case2() {
// given
OrderRequest request = createOrderRequest(10_000);
// when
Order order = Order.order(request);
// then
assertThat(order.getDiscountAmount()).isEqualTo(100);
}
@Test
@DisplayName("9999원 주문하면 할인되지 않는다.")
void case3() {
// given
OrderRequest request = createOrderRequest(9_999);
// when
Order order = Order.order(request);
// then
assertThat(order.getDiscountAmount()).isEqualTo(0);
}
}
private OrderRequest createOrderRequest(final long amount) {
return OrderRequest
.amount(amount)
.build();
}
}
4-6. 테스트 하나에는 하나의 케이스만 사용하자.
하나의 시나리오에서 2가지 이상의 상황을 이해해야 하는 경우 동료들이 테스트의 의도를 파악하는데 어려움을 겪을 수 있다.
class OrderLineServiceTest {
...
@Nested
@DisplayName("주문 아이템을 주문할 때,")
class order{
@Test
@DisplayName("아이템 개수가 2개 이하면 배송비가 3000원이고, 3개 이상이면 배송비가 무료다.")
void case() {...}
}
}
이런 경우 아래와 같이 테스트 케이스를 2개로 분리해서 작성하자.
class OrderLineServiceTest {
...
@Nested
@DisplayName("주문 아이템을 주문할 때,")
class order{
@Test
@DisplayName("아이템 개수가 2개 이하면 배송비가 3000원이다.")
void case1() {...}
@Test
@DisplayName("아이템 개수가 3개 이상이면 배송비가 무료다.")
void case2() {...}
}
}
4-7. DAMP 원칙
테스트는 읽기 좋아야 한다. 읽기 좋으려면 문맥이 제공되어야 한다. 따라서 테스트 코드를 작성할 때, DRY(Don't Repeat Yourself) 원칙이 아닌 DAMP(Descriptive And Meaningful Phrase) 원칙을 따르는 것이 좋다. 오히려 코드가 중복되고 코드 라인이 늘어나더라도 테스트 코드 이해에 도움이 된다면 그렇게 작성하는 것이 좋다.
이전 글인 [Test] Persistence Layer Test와 테스트에 대한 고찰 에서 적었다시피 아래 원칙을 지키면서 테스트를 작성해보자.
- 모든 Fixture는 given에 작성하자. (@BeforeAll이나 @BeforeEach에서 Fixture 작성을 지양)
- 생성 로직(of 등의 도메인 객체 메소드)을 사용하지 말고 생성자나 빌더를 사용해 객체를 생성하자.
- Fixture 함수를 분리하고 싶다면 한 곳에 모아서 사용하기보다는 각 테스트 클래스에 필요한 함수를 두자. 각 테스트 케이스마다 전달해야 하는 파라미터가 달라질 수 있기 때문이다.
그렇다고 필요 없는 Fixture를 given에 넣으면 오히려 혼란을 야기할 수 있으니 필요한 데이터만 선별해서 작성하도록 하자.
5. 글을 마치며
비즈니스 로직 테스트 가장 중요한 테스트다. 비즈니스 요구사항이 충족되었는지, 요구사항 자체에 결함은 없는지, 의도하지 않은 사이드 이펙트가 발생하지 않는지 확인할 수 있는 과정이기 때문이다.
그 뿐만 아니라 테스트 코드는 내가 작성한 코드에 자체 피드백을 줄 수 있다. 간혹가다 테스트 작성이 어려운 코드를 만나곤 하는데, 이는 내 코드에 문제가 있을 수 있음을 암시한다. 코드의 책임과 역할이 적절하게 할당하지 않았기 때문일 수 있다.
만약 작성한 테스트가 문서의 역할까지 할 수 있다면, 개발자들은 테스트 코드만 보고도 비즈니스의 핵심적인 규칙이나 프로세스를 파악할 수 있을 것이다. 새로운 팀원이 합류했을 때 그 팀원이 비즈니스 규칙을 이해하는 데 큰 도움을 줄 것이기 때문에 코드의 유지보수 측면에도 큰 가치가 있다.
참고자료
- Test Double을 알아보자 - Tecoble
- Mockito와 BDDMockito는 뭐가 다를까? - Tecoble
- BDD에 대한 간략한 정리
- 효율적인 테스트를 위한 Stub 객체 사용법 - 당근 테크 블로그
- Unit Test - 마틴 파울러
- 실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 2: 테스트 코드로부터 피드백받기 - 카카오페이 기술 블로그
관련된 글