ν μ€νΈ μ£Όλ κ°λ°(Test Driven Development, TDD)μ κΈ°λ₯ ꡬνμ μμ ν μ€νΈ μ½λλ₯Ό λ¨Όμ μμ±νλ μννΈμ¨μ΄ κ°λ° λ°©λ²λ‘ μ λλ€. ν μ€νΈ μΌμ΄μ€λ₯Ό λ¨Όμ μμ±νκ³ μ΄λ₯Ό ν΅κ³Όμν¬ μ΅μνμ μ½λλ₯Ό μμ±νλ κ³Όμ μ λ°λ³΅νλ©° κ°λ°μ μ§νν©λλ€.
3λ μ Spring κ³΅λΆ μ€ ν μ€νΈ μ½λλ₯Ό μ νκ³ , μ΄ν μ¬μ΄λ νλ‘μ νΈμμ PR ν νλ¦Ώμ λ§μΆμ΄ λ¨μ κ°μ λμΆ© λ°κΏ λ£λ μ λλ‘λ§ μ¬μ©νμ΅λλ€. 리뷰 μμ΄ 'λ΄ λ§λλ‘' μμ±νκΈ°μ, TDD λ°©μμ μμμ§λ§ ν μ€νΈ μ½λ μμ±μ΄ λ¨μ λ°λ³΅μ μ΄κ³ μ¬λ―Έμλ μΌλ‘ λκ»΄μ‘μ΅λλ€. νΉν 'ν μ€νΈ μ½λλΆν° κ°λ°'νλ€λ λ°©μμ΄ νμ€μ μΌλ‘ μμλμ§ μμμ΅λλ€.
//DB μ μ₯ λ©μλκ° νΈμΆλλμ§ νμΈ
verify(repository).save(any())
//λ°νλ κ°μ²΄κ° nullμ΄ μλμ§
assertNotNull(result)
//λ°ν κ°μ²΄μ ν΅μ¬ νλκ° μ ννμ§
assertEquals(expectedValue, result.getField())
//νΉμ νλ κ°μ΄ λΉμ¦λμ€ λ‘μ§μ λ§λμ§
assertTrue(result.isValid())
//μμ λ©μλκ° μ νν 1λ² νΈμΆλμλμ§ νμΈ
verify(repository, times(1)).deleteById(id)
//μμ ν μ‘°ν μ μμΈκ° λ°μνλμ§ νμΈ
assertThrows(NotFoundException.class, () -> service.read(id))
//201 μλ΅ νμΈ
assertEquals(HttpStatus.CREATED, response.getStatusCode())
//DBμμ μ§μ μ‘°ννμ¬ κ°μ²΄κ° μ‘΄μ¬νλμ§ νμΈ.
assertNotNull(repository.findById(id).orElse(null))
//μλ΅ JSON νλ κ° κ²μ¦
assertEquals(expectedValue, response.getBody().getFieldName())
//DB μ‘°ν κ²°κ³Ό Optionalμ΄ λΉμ΄μλμ§ νμΈ
assertTrue(repository.findById(id).isEmpty())

E2E λ° ν΅ν© ν μ€νΈ κ°λ° μ€ λΉλ μκ°μ΄ 5λΆ μ΄μ μμλλ νμμ΄ λ°μνμ΅λλ€. μμΈμ μ°ΎκΈ° μμνμ΅λλ€.
| νΉμ§ | @Spy (Mockito) | @SpyBean (Spring Boot Test) |
|---|---|---|
| νλ μμν¬ | Mockito | Spring Boot Test |
| μ¬μ© νκ²½ | λ¨μ ν μ€νΈ (Unit Test) | ν΅ν© ν μ€νΈ (Integration Test) |
| κ°μ²΄ λμ | κ°λ°μκ° newλ‘ μμ±ν μΌλ° μΈμ€ν΄μ€ | Spring 컨ν μ€νΈ λ΄μ κΈ°μ‘΄ Bean |
| μ£Όμ λ°©μ | @InjectMocksλ₯Ό ν΅ν μλ μ£Όμ
| @Autowiredλ₯Ό ν΅ν Springμ μλ μ£Όμ
|
| μν₯ λ²μ | ν μ€νΈ λ©μλ/ν΄λμ€ λ΄λΆ | Spring Application Context μ 체 |
@SpyBean μ¬μ© μμ κ³Ό μλ μ ν μμ μ΄ λΉμ·νμ¬ μμ¬νμ§λ§, μ΄λ κ·Όλ³Έ μμΈμ΄ μλμ νμΈνμ΅λλ€.
Dockerμ Testcontainersκ° λμμ§λ λ‘κ·Έλ₯Ό ν΅ν΄ Docker κ΄λ ¨ λ¬Έμ μμ μΆμ νμ΅λλ€.
β
ν΄κ²° κ³Όμ
1) Docker Desktop μ
λ°μ΄νΈ
2) Testcontainers μμ΄μ½μ λ°κ²¬νκ³ μ€ν μ€μΈ 컨ν
μ΄λλ₯Ό λͺ¨λ μμ νμ΅λλ€.
3) Containers running locally > Embedded Runtimeμ desktop-linuxλ‘ λ³κ²½νμ΅λλ€.
4) μ€μ λ³κ²½ ν ν
μ€νΈ μ€ν μλκ° νμ ν λΉ¨λΌμ‘μ΅λλ€.
μ±λ₯ λ° μ€λ²ν€λ (M1/Mac νκ²½μ κ²½μ° λ μ€μ)
Embedded Runtime (리λ
μ€ λ€μ΄ν°λΈ): Testcontainersλ λ컀 APIλ₯Ό μ¬μ©νμ¬ μ»¨ν
μ΄λλ₯Ό μ§μ μ€νν©λλ€. νμΌ μμ€ν
μ κ·Όμ΄ νΈμ€νΈ 리λ
μ€ μ»€λ μμ€μμ μ΄λ£¨μ΄μ§κΈ° λλ¬Έμ μ€λ²ν€λκ° κ±°μ μμ΄ λΉλ λ° ν
μ€νΈ μλκ° κ°μ₯ λΉ λ¦
λλ€.
Docker Desktop (Mac/Windows): Macμ΄λ Windowsμμ Docker Desktopμ μ¬μ©νλ©΄, λ컀 λ°λͺ¬μ νμ λ΄λΆ VM λ΄μμ μ€νλ©λλ€. νΉν M1/M2/M3 Macμ κ²½μ°, μ±λ₯ κ°μ μ΄ μ΄λ£¨μ΄μ‘μ§λ§ μ¬μ ν λ€μ΄ν°λΈ 리λ μ€μ λΉνλ©΄ μ½κ°μ νμΌ I/O μ€λ²ν€λκ° λ°μν μ μμ΅λλ€.
μ νκ²½(M1)μμλ μμκ³Ό λ¬λ¦¬ Embedded Runtimeμμ λ μ€λ 걸리λ νμμ κ²½ννμ΅λλ€. μ΄λΆλΆμ μ’λ 곡λΆν΄λ΄μΌ μ§μ μ μΈ λ¬Έμ λ₯Ό μμ μμκ² κ°μ΅λλ€.

μ΄λ² κ²½νμ ν΅ν΄ μκ² λ μ£Όμ κ°λ κ³Ό λꡬλ₯Ό μ 리νμ΅λλ€. μΆν μ ν©ν λꡬλ₯Ό νμ©ν΄ λ€μν ν μ€νΈ μ½λλ₯Ό κ²½νν΄ λ³΄λ €κ³ ν©λλ€.
π’ ν μ€νΈ λλΈ (Test Double)
- Dummy β ν μ€νΈμ νμνμ§λ§ μ€μ λ‘ μ¬μ©λμ§ μλ λλ―Έ κ°μ²΄
- Stub β νΉμ λ©μλ νΈμΆ μ 미리 μ μλ κ° λ°ν
- Fake β μ€μ ꡬνκ³Ό λΉμ·νμ§λ§ λ¨μνλ κ°μ²΄ (μ: In-memory DB)
- Mock β νΈμΆ μ¬λΆ, μΈμ λ±μ κ²μ¦ν μ μλ κ°μ²΄
- Spy β μ€μ κ°μ²΄λ₯Ό κ·Έλλ‘ μ¬μ©νλ©΄μ μΌλΆ λμλ§ κ°μ/κ²μ¦
π§ͺ E2E ν μ€νΈ
TestRestTemplateβ Spring Boot μλ² λμ HTTP μμ²/μλ΅ κ²μ¦WebTestClientβ WebFlux, μ€μ HTTP νΈμΆ- RestAssured β REST API E2E ν μ€νΈ
π§ ν΅ν© ν μ€νΈ@SpringBootTestβ μ 체 μ€νλ§ μ»¨ν μ€νΈTestContainersβ DB/Kafka/Redis μ°λ
π§± λ¨μ ν μ€νΈ
Repository/Service/ControllerJUnit/KotestMock/Spy(Mockito)@ParameterizedTest/@ValueSource
π μ¬λΌμ΄μ€ ν μ€νΈ
@WebMvcTestβ Controllerλ§, Service/Repo Mock@DataJpaTestβ Repository/JPA@RestClientTestβ RestTemplate/WebClient@JsonTestβ JSON μ§λ ¬ν/μμ§λ ¬νMockMvcβ 컨νΈλ‘€λ¬ ν μ€νΈ
π¨ μ½λ ν¬λ§· / μ€νμΌ
Spockβ BDD μ€νμΌ
π ν μ€νΈ λ°μ΄ν°
Fixture Monkeyβ μ½λ κΈ°λ° ν½μ€μ² μμ±DBRiderβ DB κΈ°λ° ν½μ€μ² μμ±
M1 νκ²½μμ @SpringBootTest μλμ λν νΈλ¬λΈμν μ λν κΈ°λ‘μ΄ μΈμμ μ΄λ€μ!