|
| 1 | +--- |
| 2 | +layout : post |
| 3 | +title : 효과적인 테스트 커버리지 관리: 비즈니스 로직에 집중하기 |
| 4 | +summary : 의미 있는 테스트 커버리지를 달성하기 위한 전략과 실천 방안을 알아봅니다. |
| 5 | +date : 2025-04-09 10:30:00 +0900 |
| 6 | +tags : [Testing, JUnit, Jacoco, TDD, Java, SpringBoot, CleanCode] |
| 7 | +toc : true |
| 8 | +comment : true |
| 9 | +public : true |
| 10 | +adsense : true |
| 11 | +--- |
| 12 | + |
| 13 | +* TOC |
| 14 | +{:toc} |
| 15 | + |
| 16 | +# 효과적인 테스트 커버리지 관리: 비즈니스 로직에 집중하기 |
| 17 | + |
| 18 | +## 1. 문제 인식 |
| 19 | + |
| 20 | +많은 개발 팀들이 테스트 커버리지 80% 이상을 목표로 삼고 있습니다. 하지만 이런 수치적 목표는 몇 가지 문제를 야기할 수 있습니다: |
| 21 | + |
| 22 | +### 1.1. 의미 없는 테스트 코드 증가 |
| 23 | + |
| 24 | +```java |
| 25 | +@Test |
| 26 | +void dto_getter_테스트() { // 이런 테스트가 필요할까요? |
| 27 | + ProductDto dto = new ProductDto("상품", 1000); |
| 28 | + assertThat(dto.getName()).isEqualTo("상품"); |
| 29 | + assertThat(dto.getPrice()).isEqualTo(1000); |
| 30 | +} |
| 31 | + |
| 32 | +@Test |
| 33 | +void config_빈_등록_테스트() { // 스프링이 이미 보장하는 것을 우리가 테스트해야 할까요? |
| 34 | + @Configuration |
| 35 | + class TestConfig { |
| 36 | + @Bean |
| 37 | + public RestTemplate restTemplate() { |
| 38 | + return new RestTemplate(); |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + var context = new AnnotationConfigApplicationContext(TestConfig.class); |
| 43 | + assertThat(context.getBean(RestTemplate.class)).isNotNull(); |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +### 1.2. 테스트 유지보수 비용 증가 |
| 48 | + |
| 49 | +- 의미 없는 테스트들로 인해 빌드 시간 증가 |
| 50 | +- 설정 변경 시 불필요한 테스트 수정 필요 |
| 51 | +- 테스트 코드 리뷰에 들이는 시간 낭비 |
| 52 | + |
| 53 | +### 1.3. 잘못된 품질 지표 |
| 54 | + |
| 55 | +```java |
| 56 | +@Getter |
| 57 | +@Setter |
| 58 | +public class SimpleDto { |
| 59 | + private String field1; |
| 60 | + private String field2; |
| 61 | + // ... 50개의 필드 |
| 62 | +} |
| 63 | + |
| 64 | +// 이런 테스트는 커버리지는 높여주지만 품질 향상에는 기여하지 않습니다 |
| 65 | +@Test |
| 66 | +void 모든_필드_테스트() { |
| 67 | + SimpleDto dto = new SimpleDto(); |
| 68 | + dto.setField1("value1"); |
| 69 | + assertThat(dto.getField1()).isEqualTo("value1"); |
| 70 | + // ... 50개 필드 모두 테스트 |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +## 2. 해결 방안: 의미 있는 테스트 커버리지 전략 |
| 75 | + |
| 76 | +### 2.1. 테스트 제외 대상 명확히 하기 |
| 77 | + |
| 78 | +```gradle |
| 79 | +jacocoTestReport { |
| 80 | + afterEvaluate { |
| 81 | + classDirectories.setFrom(files(classDirectories.files.collect { |
| 82 | + fileTree(dir: it, exclude: [ |
| 83 | + "**/Q*.class", // Querydsl |
| 84 | + "**/*Test*.*", // 테스트 클래스 |
| 85 | + "**/generated/**", // OpenAPI 생성 코드 |
| 86 | + "**/dto/**", // DTO 클래스 |
| 87 | + "**/config/**", // 설정 클래스 |
| 88 | + "**/Application.class" // 메인 애플리케이션 클래스 |
| 89 | + ]) |
| 90 | + })) |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +### 2.2. 테스트가 필요한 영역 식별하기 |
| 96 | + |
| 97 | +#### 2.2.1. 반드시 테스트해야 할 것 |
| 98 | + |
| 99 | +1. **도메인 로직이 있는 Entity** |
| 100 | +```java |
| 101 | +@Entity |
| 102 | +public class Order { |
| 103 | + public void apply(Coupon coupon) { |
| 104 | + if (!coupon.isValidFor(this)) { |
| 105 | + throw new InvalidCouponException(); |
| 106 | + } |
| 107 | + this.discount = coupon.calculateDiscount(this.amount); |
| 108 | + this.couponUsed = true; |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +@Test |
| 113 | +void 주문에_쿠폰_적용시_할인금액이_정확히_계산되어야_한다() { |
| 114 | + // given |
| 115 | + var order = OrderTestFixture.createWithAmount(10000); |
| 116 | + var coupon = CouponTestFixture.createPercentage(10); // 10% 할인 |
| 117 | + |
| 118 | + // when |
| 119 | + order.apply(coupon); |
| 120 | + |
| 121 | + // then |
| 122 | + assertThat(order.getDiscount()).isEqualTo(1000); |
| 123 | + assertThat(order.isCouponUsed()).isTrue(); |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +2. **비즈니스 로직이 있는 Service** |
| 128 | +```java |
| 129 | +@Test |
| 130 | +void 재고가_부족한_상품_주문시_실패해야_한다() { |
| 131 | + // given |
| 132 | + var product = ProductTestFixture.createWithStock(0); |
| 133 | + var orderRequest = OrderRequestTestFixture.create(product.getId(), 1); |
| 134 | + |
| 135 | + // when & then |
| 136 | + assertThatThrownBy(() -> orderService.createOrder(orderRequest)) |
| 137 | + .isInstanceOf(OutOfStockException.class); |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +#### 2.2.2. 테스트가 불필요한 것 |
| 142 | + |
| 143 | +1. **단순 DTO** |
| 144 | +```java |
| 145 | +@Getter |
| 146 | +@AllArgsConstructor |
| 147 | +public class ProductResponse { |
| 148 | + private String name; |
| 149 | + private BigDecimal price; |
| 150 | + // 단순 데이터 전달 객체는 테스트가 불필요 |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +2. **설정 클래스** |
| 155 | +```java |
| 156 | +@Configuration |
| 157 | +public class WebConfig implements WebMvcConfigurer { |
| 158 | + @Override |
| 159 | + public void addCorsMappings(CorsRegistry registry) { |
| 160 | + registry.addMapping("/**") |
| 161 | + .allowedOrigins("*"); |
| 162 | + } |
| 163 | + // 프레임워크가 보장하는 동작은 테스트 불필요 |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +## 3. 실천 방안 |
| 168 | + |
| 169 | +### 3.1. 테스트 우선순위 정하기 |
| 170 | + |
| 171 | +1. 높은 우선순위 |
| 172 | + - 핵심 비즈니스 로직 |
| 173 | + - 도메인 규칙 |
| 174 | + - 중요한 유효성 검사 |
| 175 | + |
| 176 | +2. 중간 우선순위 |
| 177 | + - 인프라 코드 |
| 178 | + - 외부 시스템 연동 |
| 179 | + |
| 180 | +3. 낮은 우선순위 |
| 181 | + - 설정 클래스 |
| 182 | + - 단순 DTO |
| 183 | + - 자동 생성 코드 |
| 184 | + |
| 185 | +### 3.2. 테스트 가치 평가 체크리스트 |
| 186 | + |
| 187 | +- [ ] 이 테스트는 비즈니스 요구사항을 검증하는가? |
| 188 | +- [ ] 이 테스트가 실패하면 실제 문제가 있다는 것을 의미하는가? |
| 189 | +- [ ] 이 테스트는 코드의 동작을 명확하게 설명하는가? |
| 190 | +- [ ] 이 테스트는 유지보수 비용 대비 충분한 가치가 있는가? |
| 191 | + |
| 192 | +## 4. 결론 |
| 193 | + |
| 194 | +테스트 커버리지는 코드 품질을 측정하는 하나의 지표일 뿐입니다. 진정한 목표는 신뢰할 수 있는 소프트웨어를 만드는 것입니다. 따라서: |
| 195 | + |
| 196 | +1. 의미 있는 비즈니스 로직에 집중하여 테스트를 작성하세요 |
| 197 | +2. 자동 생성된 코드나 단순 구조체는 과감히 제외하세요 |
| 198 | +3. 테스트의 가치를 항상 고민하고 평가하세요 |
| 199 | + |
| 200 | +이를 통해 테스트는 단순한 커버리지 수치가 아닌, 실제 코드 품질 향상에 기여할 수 있습니다. |
| 201 | + |
| 202 | +> "테스트의 목적은 버그를 찾는 것이 아니라, 코드가 의도대로 동작한다는 확신을 주는 것입니다." |
| 203 | +
|
| 204 | +## 참고 자료 |
| 205 | + |
| 206 | +- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) |
| 207 | +- [Jacoco Documentation](https://www.jacoco.org/jacoco/trunk/doc/) |
| 208 | +- [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) |
| 209 | +- [Test Driven Development: By Example](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530) |
| 210 | + |
0 commit comments