이번 글에서는 박우빈님의 "Practical Testing: 실용적인 테스트 가이드"를 보고 테스트 코드에 대한 제 생각을 정리해 보려고 합니다.
내가 작성한 실제 코드가 제대로 동작하는지 검증하는 코드입니다.
테스트 코드와 관련된 TDD, BDD와 같은 개발 방법론이 존재합니다.
자바 진영에서는 JUnit 프레임워크를 사용해서 테스트 코드를 작성할 수 있습니다.
외부 기술을 공부할 때 테스트 코드를 사용하면 여러 가지 경우를 직접 설정해 구체적인 기능을 재밌게 학습할 수 있는 장점도 있습니다.
제가 현재 겪고 있는 일입니다.
저는 현재 팀 프로젝트를 진행하고 있습니다.
이 프로젝트에서는 테스트 코드를 전혀 작성하지 않고 서비스 코드 작성 후 postman을 사용해서 성공 사례와 간단한 예외 케이스만 확인한 후 바로 배포해서 프론트 분들께서 이어서 개발을 진행하십니다.
당시에는 테스트 코드의 중요성을 못 느껴서 작성하지 않고 진행했었는데 지금 와서 보니 오류가 언제 발생할지 모르는 프로젝트가 되었고 개발하는 시간보다 예상치 못한 곳에서 터지는 오류(예외 처리, 유효성 검증 등)를 고치는 시간이 더 많아졌습니다.
결국 테스트 코드가 없으면 내 코드가 의도한 대로 동작하는지 검증할 방법이 없어 경험과 감에 의존한 채 개발을 진행하게 되고 결국에는 불안정한 서비스가 될 것입니다.
그리고 오류를 배포 후에야 확인할 수 있어 서비스에 대한 피드백이 늦고 유지보수는 점점 어려워지고 서비스의 품질이 떨어질 것입니다.
@SpringBootTest // 서버를 띄워 통합 테스트를 진행
class ProductServiceTest {
@Autowired
private ProductService productService;
@Autowired
private ProductRepository productRepository;
@AfterEach // 각 테스트가 끝난 후 실행
void tearDown() {
productRepository.deleteAllInBatch();
}
@DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1 증가한 값이다.")
@Test
void createProduct() {
// given
Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
productRepository.save(product);
ProductCreateServiceRequest request = ProductCreateServiceRequest.builder()
.type(HANDMADE)
.sellingStatus(SELLING)
.name("카푸치노")
.price(5000)
.build();
// when
ProductResponse productResponse = productService.createProduct(request);
// then
assertThat(productResponse)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains("002", HANDMADE, SELLING, "카푸치노", 5000);
List<Product> products = productRepository.findAll();
assertThat(products).hasSize(2)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.containsExactlyInAnyOrder(
tuple("001", HANDMADE, SELLING, "아메리카노", 4000),
tuple("002", HANDMADE, SELLING, "카푸치노", 5000)
);
}
@DisplayName("상품이 하나도 없는 경우 신규 상품을 등록하면 상품번호는 001이다.")
@Test
void createProductWhenProductIsEmpty() {
// given
ProductCreateRequest request = ProductCreateRequest.builder()
.type(HANDMADE)
.sellingStatus(SELLING)
.name("카푸치노")
.price(5000)
.build();
// when
ProductResponse productResponse = productService.createProduct(request.toServiceRequest());
// then
assertThat(productResponse)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains("001", HANDMADE, SELLING, "카푸치노", 5000);
List<Product> products = productRepository.findAll();
assertThat(products).hasSize(1)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains(
tuple("001", HANDMADE, SELLING, "카푸치노", 5000)
);
}
상품을 등록하는 실제 코드를 테스트하는 코드입니다.
BDD 방식(given-when-then)으로 작성했습니다.
상품을 등록하는 기능을 포함해서 전체적인 서비스를 출시하고 유지보수를 해야 한다는 가정을 해보겠습니다.
그렇게 되면 서비스 인수인계를 해야할 때가 올 것입니다.
서비스를 넘겨받은 개발자는 서비스 코드를 이해하는 시간이 필요한 데,
이때 테스트 코드가 도움을 줄 수 있습니다.
테스트 코드를 보면 당시 개발자가 어떤 흐름으로 서비스를 구성했는지 알 수 있습니다.
결국 테스트 코드가 있게 되면 서비스를 넘겨받은 개발자는 쉽게 서비스에 적응할 수 있을 것입니다.
그리고 서비스를 유지 보수하는 개발자도 서비스를 더 잘 이해하면서 코드를 작성할 수 있습니다.
테스트 코드가 없어 코드를 바로 배포하게 되면 예상치 못한 오류 (예외 처리를 하지 않았거나, 유효성 검증을 하지 않거나 등등 ...) 를 만나게 될 확률이 높습니다. 그렇게 되면 서비스 품질과 서비스에 대한 사용자의 신뢰도가 굉장히 낮아질 것입니다.
하지만 테스트 코드를 작성하면 실제 코드가 의도한 대로 동작하는지 바로 확인할 수 있어 예상치 못한 오류를 만날 확률이 낮습니다.
테스트 코드가 의도한 대로 동작하면 위 이미지처럼 초록 불이 뜰 것입니다.
테스트 코드를 제대로 작성했다는 가정하에,
실제 코드를 과감하게 리팩토링해도 초록 불만 확인하면 코드가 제대로 작동하는지 검증할 수 있습니다.
실제 코드 작성 -> 테스트 코드 작성 -> 초록 불 확인 -> 리팩토링 -> 초록 불 확인 ...
위 사이클을 반복하면서 서비스를 구축하고 리팩토링하면
리팩토링 시간을 단축하고 전보다 클린한 코드를 작성할 수 있을 것입니다.
테스트 코드가 있으면 개발자는 언제든지 테스트 코드를 실행해서 실제 코드가 의도한 대로 동작하는지 확인할 수 있습니다.
유지보수하는 과정에서 코드가 수정되는 부분이 있을 수 있는데 이럴 때마다 테스트 코드를 실행해 실제 코드가 잘 작동하는 걸 확인하면 안정적인 서비스를 구축할 수 있을 것입니다.
또한 빠르게 변화하는 소프트웨어의 안정성을 테스트 코드를 통해 보장할 수 있습니다.
일단 테스트 코드에 관해 공부하면서 전에 테스트 코드를 작성하지 않던 저 자신이 점점 미워졌습니다 😭😭😭
계층형 아키텍처에서 헥사고날 아키텍처 도입하기
당시에도 테스트 코드가 없어 수동으로 테스트를 진행해서 리팩토링하기가 굉장히 힘들었었습니다.
저는 개발자에게 중요한 것 중 하나는 자신이 하는 일에서 반복되는 일을 자동화해서 시간을 단축하고 실수를 줄이는 게 중요하다고 생각합니다.
테스트 코드는 제가 수동으로 테스트한 것을 이른 시간 안에 정확하게 자동으로 확인해 주기 때문에 좋은 서비스를 개발할 수 있도록 해주는 것 같습니다.
테스트 코드는 공부하면서 느끼는 거지만 정말 필수인 것 같습니다.
이 글을 읽고 있는 다른 개발자분들도 프로젝트에 테스트 코드를 작성해서 다양하고 좋은 경험을 해보시면 좋을 것 같습니다!