DevLog ๐ถ
[Spring] ํตํฉ ํ ์คํธ ํ๊ฒฝ์์ ๋ก์ง์ ์ผ๋ถ๋ฅผ stubbing ํ๊ธฐ ๋ณธ๋ฌธ
[Spring] ํตํฉ ํ ์คํธ ํ๊ฒฝ์์ ๋ก์ง์ ์ผ๋ถ๋ฅผ stubbing ํ๊ธฐ
dolmeng2 2024. 6. 16. 19:27
๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
ํตํฉ ํ
์คํธ๋ฅผ ํ๋ค ๋ณด๋ฉด, ์ค์ ๋น ์ค์ ์ผ๋ถ๋ง stubbing์ ํ๊ณ ์ถ์ ์ํฉ์ด ์๊ธด๋ค.
์๋ฅผ ๋ค์ด, ์ธ๋ถ API๋ฅผ ํต์ ํ๊ฑฐ๋ ์ธ๋ถ ์์คํ
์ ํ์ฉํ๊ฒ ๋๋ฉด ํ
์คํธ์์ ๊ทธ๋๋ก ํ์ฉํ๊ธฐ ์ด๋ ค์ด๋ฐ, ํด๋น ๋ถ๋ถ์ ๋ํด์๋ง ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์๋๋ฆฌ์ค๋ฅผ ์ ๊ณตํ์ฌ ์ฐ๋ฆฌ ์ ํ๋ฆฌ์ผ์ด์
์ด ์ ๋์ํ๋์ง ํ์ธํ๊ณ ์ถ์ ์ ์๋ค. ํ์ง๋ง, ๋ณดํต Spring Context์์๋ ๋์ผํ ์ด๋ฆ์ ๊ฐ์ง ๋น์ ๊ทธ๋๋ก ๋ฑ๋กํ๊ฒ ๋๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์, ๋ช ๊ฐ์ง ํน๋ณํ ์กฐ์น๋ฅผ ํด์ฃผ์ด์ผ ํ๋๋ฐ ํ ๋ฒ ์ดํด๋ณด์.
โ๏ธ ํ ์คํธ ์๋๋ฆฌ์ค
- ์ฌ์ฉ์๊ฐ ์ํ์ ๊ตฌ๋งคํ๊ฒ ๋๋ฉด, ์ธ๋ถ API๋ฅผ ํธ์ถํ์ฌ ์ฌ๊ณ ๊ฐ ์กด์ฌํ๋์ง ํ์ธํ๋ค.
- ๋ง์ฝ ์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ฉด ์ธ๋ถ API๋ ‘SUCCESS’๋ผ๋ ์๋ต์, ์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด ‘FAIL’์ด๋ผ๋ ์๋ต์ ๋ฐํํ๋ค.
- ์ฐ๋ฆฌ๋ ์ฌ๊ธฐ์์ ‘์ธ๋ถ API๊ฐ SUCCESS ํน์ FAIL’์ด๋ผ๋ ์๋ต์ ๋ฐํํ๋ ๋ถ๋ถ์ ๋ํด์ stubbing์ ์งํํ ๊ฒ์ด๋ค.
์์ ์๋๋ฆฌ์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๊ฐ๋จํ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์.
๋ณธ ๊ธ์์๋ ์ํคํ
์ฒ๋ ์ธ๋ถ ์ฝ๋์ ๋ํ ๊ตฌํ์ ์ค์ํ์ง ์๊ธฐ ๋๋ฌธ์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ฝ๋๋ก ์์ฑํ์๋ค.
โ๏ธ ๊ธฐ๋ณธ ์ฝ๋
@RestController
class OrderController(
private val orderService: OrderService,
) {
@PostMapping("/order")
fun order(
@RequestBody request: OrderRequest
) {
orderService.order(request.productId)
}
}
data class OrderRequest(
val productId: Long,
)
@Service
class OrderService(
private val externalStockCheckApi: ExternalStockCheckApi
) {
fun order(productId: Long) {
val result = externalStockCheckApi.check(productId)
if (result == StockStatus.FAIL) {
throw OutOfStockException("์ฌ๊ณ ๊ฐ ์์ต๋๋ค.")
}
otherLogic()
}
private fun otherLogic() {
// do something...
}
}
class OutOfStockException(
override val message: String
) : RuntimeException(message) {
}
์ฌ๊ธฐ์์ otherLogic์ ๊ฒฝ์ฐ ๋งค์ฐ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋๋ค๊ณ ๊ฐ์ ํ๋ค.
์คํจ์ ์ผ์ด์ค์ ๋ํด์๋ ๋ณต์กํ ๊ฒฝ์ฐ๊ฐ ํฌํจ๋์ง๋ง, ์ฌ๊ธฐ์์๋ ๊ฐ๋จํ๊ฒ ์์ธ๋ง ๋ฐ์์ํค๋๋ก ์ ์ด๋ฅผ ํด๋์๋ค.
@Component
class ExternalStockCheckApi {
fun check(productId: Long): StockStatus {
return callExternalApi(productId)
}
private fun callExternalApi(productId: Long): StockStatus {
// call external api...
if (Random().nextInt() % 2 == 0) {
return StockStatus.SUCCESS
}
return StockStatus.FAIL
}
}
enum class StockStatus {
SUCCESS,
FAIL
}
ExternalStockCheckApi ์ ๊ฒฝ์ฐ, ์ค์ ๋ก๋ ์ธ๋ถ API์ ํต์ ํ๋ ๊ตฌ๊ฐ์ด๋ค.
ํ์ฌ ์ฐ๋ฆฌ์ ์ฝ๋์์๋ ํด๋น ๋ถ๋ถ์ ์ค์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์ ์ธํ๊ณ , ๊ทธ๋ฅ ๋๋คํ ๊ฐ์ ๋ฐ๋ผ์ ์ฑ๊ณต๊ณผ ์คํจ์ ๋ํด์ ๋ฐํํ๋๋ก ์์ ํ์๋ค.
์ฐ๋ฆฌ๋ ์์ ๊ฐ์ ์ฝ๋๊ฐ ์์ ๋, `ExternalStockCheckApi`์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ดํ์ฌ ์ฐ๋ฆฌ์ ์ ํ๋ฆฌ์ผ์ด์
์ด ์๋ํ๋๋๋ก ์ ๋์ํ๋์ง๋ฅผ ํ์ธํด๋ณผ ์์ ์ด๋ค. ๋ํ, ๋จ์ ํ
์คํธ๊ฐ ์๋ ํตํฉ ํ
์คํธ๋ฅผ ํ์ฉํ ์์ ์ด๋ค. ๋ฌผ๋ก , ํ
์คํธ์ ๋ฐฉ๋ฒ์๋ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์๊ณ , ๋จ์ ํ
์คํธ๋ฅผ ํ์ฉํด์ ์ฐ๋ฆฌ์ ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฒ์ฆํ๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค.
๊ทธ๋ฌ๋, ์ค์ ๋ก ๋ณต์กํ ๋น์ฆ๋์ค๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฒฝ์ฐ์๋ ์๋นํ ๋ง์ ๋น๋ค์ด ์๋ก ํ๋ ฅํ์ฌ ๋์ํ๊ฒ ๋๋ค. ๋ชจ๋ ๋น์ ๋ํด์ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ์ฌ ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ๋ค์ ๋ํ ์ ์์ ์ธ ๋์์ ํ๋จํ ์๋ ์์ง๋ง, ํด๋น ์ปดํฌ๋ํธ๋ค์ ๋จ์ ํ ์คํธ๊ฐ ์์ฑ๋์ง ์์ ์ํฉ์ด๊ฑฐ๋ ๊ฐ ์ปดํฌ๋ํธ๋ค๊ฐ์ ์ ์์ ์ธ ํ๋ ฅ ๊ด๊ณ๊ฐ ์ ๋์ํ๊ณ ์๋์ง ํ๋จํ๊ณ ์ถ์ ๋๋ ํตํฉ ํ ์คํธ๊ฐ ์ข์ ๋ฐฉ๋ฒ์ด ๋ ์ ์๋ค.
๊ทธ๋ ๋ค๋ฉด, ‘ํ ์คํธ ํ๊ฒฝ์์๋ ์ค์ ์ธ๋ถ API๋ฅผ ํธ์ถํ๋ฉด ๋์ง ์๋๊ฐ?’ ๋ผ๊ณ ์๊ฐํ ์ ์๋ค. ๊ทธ๋ฌ๋, ๋ง์ฝ ์ธ๋ถ API๊ฐ ์ผ์์ ์ผ๋ก ์ฅ์ ๊ฐ ๋ฐ์ํ์ด์ ๋ ํต๊ณผํ๋ ํ ์คํธ๊ฐ ๊ฐ์๊ธฐ ํต๊ณผํ์ง ์๋๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น? ํ ์คํธ์ ๊ฒฝ์ฐ ๋ ์ผ๊ด์ ์ธ ์๋๋ฆฌ์ค๋ก ๋์ํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ด์ ๋ํด ์๋ฐฐํ๊ฒ ๋๋ค. (FIRST ์์น, ๋ฌผ๋ก ์ด๋ ๋จ์ ํ ์คํธ์ ๋ํ ์์น์ด์ง๋ง ๊ทธ๋๋ ์ด์ง ์ธ์ฉํ์๋ฉด ๊ทธ๋ ๋ค.)
๋ํ, ์ฌ์ ์ ์ธ ๊ด์ ์ผ๋ก ๋ดค์ ๋ ์ธ๋ถ API์ ๋ํ ํธ์ถ ๋น์ฉ์ ๋ฐ๋ ๊ณณ์ด๋ผ๋ฉด? ํ ์คํธ ํ ๋๋ง๋ค ๋น์ฉ ์ง์ถ์ด ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๋ ์ ๋์ stubbing์ ํ์ํ ์ํฉ์ด๋ค. (์ค์ ๋ก ์ฌ๋ด์์ ๋์ผํ ์ผ์ด์ค๊ฐ ์์ด์, ํ ์คํธ ํ๊ฒฝ์์๋ ์์ dummy ๊ฐ์ ๋ด๋ ค์ฃผ๋๋ก ์ฒ๋ฆฌํ๋ ๋ถ๋ถ๋ ์กด์ฌํ๋ค.)
์๋ฌดํผ, ์์ ์๋๋ฆฌ์ค๋ฅผ ํ๋จํ๊ธฐ ์ํ ๊ฐ๋จํ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์. RestAssured๋ฅผ ํ์ฉํ์ฌ ์์ฑํด์ฃผ์๋ค.
ํ ์คํธ ์ค์ ๋ฐฉ๋ฒ์ ๋ํด์๋ ๋ค์ ํฌ์คํ ์์ ์กฐ๊ธ ๋ ๋ค๋ฃจ์ด๋ณด๊ณ ์ ํ๋ค.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class OrderControllerTest() {
@LocalServerPort
private var port: Int = 0
@BeforeAll
fun setUp() {
RestAssured.port = port
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(2L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}
private fun callOrderApi(
productId: Long
): ExtractableResponse<Response> {
return RestAssured.given()
.given().log().all()
.contentType("application/json")
.body(
mapOf("productId" to productId)
)
.`when`()
.post("/order")
.then().log().all().extract()
}
}
์ฐ๋ฆฌ๊ฐ ์ํ๋ ์๋๋ฆฌ์ค๋ ์์ ๊ฐ์ 2๊ฐ์ง ์ํฉ์ ํ
์คํธ ํ ์ ์๋ค.
- ์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ ์๋ฌด ์ค๋ฅ๋ ๋ฐ์ํ์ง ์์
- ์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์์ธ๊ฐ ๋ฐ์ํจ
๋น์ฐํ ํ์ฌ์ ํ
์คํธ ์ฝ๋๋ ๊นจ์ง๋ ๊ฒ์ด ๋ง๋ค.
๐ฑ @MockBean์ ํ์ฉํ์ฌ ์ฌ์ ์ํ๊ธฐ
โ๏ธ @MockBean์ด ๋ฌด์์ผ๊น?
๋จผ์ , ์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก๋ @MockBean์ ํ์ฉํ์ฌ `ExternalStockCheckApi` ์ ๋ํด ์ฌ์ ์๋ฅผ ํ ์ ์๋ค.
@MockBean์ ๊ฒฝ์ฐ Spring 1.4์์ ์ถ๊ฐ๋ ํ ์คํธ ์ํฌํ ์ฉ ์ด๋ ธํ ์ด์ ์ผ๋ก, ์คํ๋ง์ ApplicationContext์ mocking ๋ ๊ฐ์ฒด๋ฅผ ์ถ๊ฐํ๋๋ฐ ์ฌ์ฉํ๋ ์ด๋ ธํ ์ด์ ์ด๋ค. ๋น์ ํ์ ์ด๋ ์ด๋ฆ์ผ๋ก ๋ฑ๋ก์ด ๊ฐ๋ฅํ๋ฉฐ, ๊ธฐ์กด์ ์ปจํ ์คํธ์์ ์ผ์นํ๋ ๋น์ ๋ํด์ mock ๋น์ผ๋ก ๋์ฒดํ๊ฒ ๋ง๋ค์ด์ค๋ค. ๋ง์ฝ ๊ธฐ์กด์ ๋น์ด ์กด์ฌํ์ง ์๋๋ค๋ฉด ์๋ก์ด ๋น์ผ๋ก ์ถ๊ฐํ์ฌ ๋ฑ๋กํด์ค๋ค.
Junit4๋ฅผ ์ฌ์ฉํ๋ค๋ฉด `@RunWith(SpringRunner.class)`์ ํจ๊ป, Junit5๋ฅผ ์ฌ์ฉํ๋ค๋ฉด `@ExtendWith(SpringExtension.class)` ์ ํจ๊ป ์ฌ์ฉํ ์ ์๋ค. ๋๋ @SpringBootTest ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ ์์ ์ด๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ๋ถ์ฌ์ฃผ์ง๋ ์์๋ค.
์ฐธ๊ณ ๋ก, ์ด๋ฌํ @MockBean์ ๊ฒฝ์ฐ `MockitoTestExecutionListener` ์ ์ํด์ ์ฒ๋ฆฌ๋๊ณ ์๋ค.
`MockitoTestExecutionListener` ์ ๊ฒฝ์ฐ `AbstractTestExecutionListener` ์ ์์๋ฐ์์ผ๋ฉฐ, ํ ์คํธ ์ปจํ ์คํธ๋ฅผ ํ์ฉํ์ฌ ํ ์คํธ ์ฝ๋ ์ํ ์ ์ด๋ค ์ ์ฒ๋ฆฌ / ํ์ฒ๋ฆฌ๋ฅผ ์งํํ ์ง ์ ํด์ค ์ ์๋ค.
public class MockitoTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
closeMocks(testContext);
initMocks(testContext);
injectFields(testContext);
}
}
private void initMocks(TestContext testContext) {
if (hasMockitoAnnotations(testContext)) {
testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testContext.getTestInstance()));
}
}
private void injectFields(TestContext testContext) {
postProcessFields(testContext, (mockitoField, postProcessor) -> postProcessor.inject(mockitoField.field,
mockitoField.target, mockitoField.definition));
}
private void postProcessFields(TestContext testContext, BiConsumer<MockitoField, MockitoPostProcessor> consumer) {
DefinitionsParser parser = new DefinitionsParser();
parser.parse(testContext.getTestClass());
if (!parser.getDefinitions().isEmpty()) {
MockitoPostProcessor postProcessor = testContext.getApplicationContext()
.getBean(MockitoPostProcessor.class);
for (Definition definition : parser.getDefinitions()) {
Field field = parser.getField(definition);
if (field != null) {
consumer.accept(new MockitoField(field, testContext.getTestInstance(), definition), postProcessor);
}
}
}
}
testContext๋ฅผ ํ์ฉํ๋ฉด ํ
์คํธ ์ฝ๋์ ์ ์ธ๋ ์ด๋
ธํ
์ด์
์ ๋ณด๋ฅผ ์ฝ์ด์ฌ ์ ์๊ธฐ ๋๋ฌธ์, mockito ๊ธฐ๋ฐ ์ด๋
ธํ
์ด์
๋ค์ ์ฐพ์์ mock field๋ก ์ฃผ์
ํด์ฃผ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
โ๏ธ ์ค์ ๋ก ์ ์ฉํด๋ณด๊ธฐ
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class OrderControllerTest(
@MockBean
private val externalStockCheckApi: ExternalStockCheckApi
) {
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
Mockito.`when`(
externalStockCheckApi.check(anyLong())
).thenReturn(StockStatus.SUCCESS)
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค`() {
Mockito.`when`(
externalStockCheckApi.check(anyLong())
).thenReturn(StockStatus.FAIL)
val result = callOrderApi(2L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}
}
Mockito.when().thenReturn()์ ์ฌ์ฉํ๋ฉด mockBean์ผ๋ก ๋ฑ๋ก๋ mock ๊ฐ์ฒด์ ๋ํด์ ์ฝ๊ฒ stubbing์ ์งํํ ์ ์๋ค.
when์ ์๋ ์ฐ๋ฆฌ๊ฐ ์คํฐ๋น์ ์งํํ๊ณ ์ถ์ ๋ฉ์๋๋ฅผ ์ ๋ ฅํ๊ณ , then ์ ์๋ ํด๋น ์ํฉ์์ ์ด๋ค ๊ฐ์ ๋ฐํํ๋ฉด ์ข์์ง ์์ฑํด์ฃผ๋ฉด ๋๋ค. thenReturn ์ธ์๋ thenThrow, thenAnswer ๋ฑ์ผ๋ก ๋ ์ ์ฐํ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
โ๏ธ ๋จ์ ์ ์์๊น?
์์ฝ๊ฒ๋, @MockBean์ ํ์ฉํ๊ฒ ๋๋ฉด ์คํ๋ง ์ปจํ
์คํธ๋ฅผ ์ฌ์ฌ์ฉํ ์ ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ฐ์ , @MockBean์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ํ ์คํธ ํด๋์ค๋ฅผ ์ํํ ๋๋ง๋ค Spring Context์ ๋ํด refresh๋ฅผ ์งํํ๋๋ฐ, ์ด๋ ์์ ์ฒจ๋ถํด๋ `postProcessFields()` ํจ์์ ๋ค์๊ณผ ๊ฐ์ ๋ถ๋ถ์ ์ดํด๋ณด๋ฉด ์ ์ ์๋ค.
// postProcessFields
if (!parser.getDefinitions().isEmpty()) {
MockitoPostProcessor postProcessor = testContext.getApplicationContext()
.getBean(MockitoPostProcessor.class);
}
// DefaultTestContext.getApplicationContext
@Override
public ApplicationContext getApplicationContext() {
ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedConfig);
...
}
// DefaultCacheAwareContextLoaderDelegate.loadContext
@Override
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
...
synchronized (this.contextCache) {
ApplicationContext context = this.contextCache.get(mergedConfig);
...
context = loadContextInternal(mergedConfig);
...
}
protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedConfig) throws Exception {
ContextLoader contextLoader = getContextLoader(mergedConfig);
if (contextLoader instanceof SmartContextLoader smartContextLoader) {
return smartContextLoader.loadContext(mergedConfig);
}
...
}
// SpringBootContextLoader.loadContext
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer) throws Exception {
...
return hook.run(() -> application.run(args));
}
// SpringApplication.run
public ConfigurableApplicationContext run(String... args) {
refreshContext(context);
}
์ฌ๊ธฐ์์ ํ
์คํธ ์ปจํ
์คํธ ๋ด์ ์๋ applicationContext๋ฅผ ๊บผ๋ด์ค๋ ๊ฒ์ ์ ์ ์๋๋ฐ, ๋ด๋ถ๋ก ํ๊ณ ๋ค์ด๊ฐ๊ฒ ๋๋ฉด context์ ๋ํด refresh๋ฅผ ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ฝ๋์ ์ค๊ฐ ๋ถ๋ถ์ ๋ณด๋ฉด ์คํ๋ง์์๋ ์ต๋ํ ์บ์๋ ํ
์คํธ ์ปจํ
์คํธ๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํ์ง๋ง (contextCache ๊ฐ์ ํ๋๋ค์ด ์กด์ฌํ๋ ๊ฑธ ๋ณผ ์ ์๋ค), @MockBean์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ํด๋น ๋น ๋ฟ๋ง ์๋๋ผ ํด๋น ๋น์ ์ฌ์ฉํ๋ ๋ค๋ฅธ ์น๊ตฌ๋ค๋ ๋ชจ๋ ๊ต์ฒดํด์ค์ผ ํ๊ธฐ ๋๋ฌธ์ ์บ์๋ฅผ ํ์ง ์๊ณ ์์ refresh๋ฅผ ํด์ฃผ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
class A(val b: B) ์ ๊ฐ์ ์ฝ๋๊ฐ ์์ ๋, B๊ฐ mockBean์ด๋ผ๋ฉด ์์ฐ์ค๋ฝ๊ฒ B๋ฅผ ์์กดํ๊ณ ์๋ A ์ญ์ mocking๋ B๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ๊ต์ฒด ์์ ์ด ํ์ํด์ง๋ค. ๋ง์ฝ ๋น๋ผ๋ฆฌ ๋ ๋ณต์กํ ์์กด ๊ด๊ณ๋ฅผ ๊ฐ์ง๊ณ ์๋ค๋ฉด ์์ฐ์ค๋ฝ๊ฒ ๋ ๋ง์ ๋น๋ค์ ๋ํด์ ๊ต์ฒดํ๋ ์์ ์ด ํ์ํด์ง ๊ฒ์ด๋ค.
์ฐธ๊ณ ๋ก, ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ๋ง์ context caching์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์ ๋ํด์ ๋ฐ์ํ๋ค.
- ๋์ผํ bean์ ์กฐํฉ์ ์ฌ์ฉํ์ ๋
- ์ด์ ์ ํ ์คํธ์์ applicationContext๊ฐ ์ค์ผ๋์ง ์์ ๊ฒฝ์ฐ
- @DirtyContext ๋ฅผ ํ์ฉํ์ง ์๋ ๊ฒฝ์ฐ
๐ฑ @Profile ์ค์ ์ ํตํด์ ํ
์คํธ์ฉ ๋น์ ๋ถ๋ฆฌํ๊ธฐ
๋ง์ฝ, ์์ ๊ฐ์ spring context refresh๊ฐ ์ซ๋ค๋ฉด, ํ ์คํธ์์ ์ฌ์ฉํ ํ๋กํ์ผ์ ์ง์ ํด์ฃผ๊ณ ํด๋น ํ๋กํ์ผ์์๋ ๋ค๋ฅธ ๋์์ ํ๋๋ก ๋ง๋ค์ด ์ค ์ ์๋ค. ํ์ง๋ง, ์คํ๋ง์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋์ผํ ์ด๋ฆ์ ๊ฐ์ง ๋น์ 2๊ฐ ๋์ธ ์ ์๋ค. ์ด๋ฅผ ์ํด `ExternalStockCheckApi`๋ฅผ ์ธํฐํ์ด์ค๋ก ๋ถ๋ฆฌํด์ฃผ๋ ์์ ์ ์งํํด์ฃผ์. ์ธํฐํ์ด์ค์ ๊ฒฝ์ฐ ๊ตฌ์ฒด์ ์ธ ํ์๊ฐ ๋ค์ด๋์ง ์๋๋ก, ์ฌ๊ณ ๋ฅผ ํ์ธํ๋ ์ฉ๋์ ๊ธฐ๋ฅ ์ ๋๋ง ๋ํ๋ด ์ค ์ ์๋๋ก ๋ง๋ค์๋ค.
interface CheckStockPort {
fun check(productId: Long): StockStatus
}
๊ทธ๋ฆฌ๊ณ , @Profile ์ด๋
ธํ
์ด์
์ ํ์ฉํ์ฌ profile ๋ณ๋ก ๋ค๋ฅธ ์ฝ๋๊ฐ ๋์ํ ์ ์๋๋ก ๋ง๋ค์ด์ฃผ์.
@Component
@Profile("!test")
class ExternalStockCheckApi: CheckStockPort {
override fun check(productId: Long): StockStatus {
return callExternalApi(productId)
}
}
@Component
@Profile("test")
class TestStockCheckApi: CheckStockPort {
override fun check(productId: Long): StockStatus {
if (productId == 1L) {
return StockStatus.SUCCESS
}
return StockStatus.FAIL
}
}
๊ธฐ์กด์ ExternalStockCheckApi ์ ๋ํด์ CheckStockPort ์ ์ค๋ฒ๋ผ์ด๋ ํ๋๋ก ๋ง๋ค๊ณ , ํน์ Test ํ๋กํ์ผ์ ๋ํด์๋ TestStockCheckApi ๊ฐ ๋์ํ๋๋ก ๋ง๋ค์๋ค.
๊ทธ๋ฆฌ๊ณ , ์ฐ๋ฆฌ์ ํ ์คํธ ์๋๋ฆฌ์ค์ ๋ง์ถฐ productId ๋ณ๋ก ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋ํ๋ ์ ์๋๋ก ๋ง๋ค์๋ค.
๊ธฐ์กด์ ExternalStockApi๋ฅผ ์์กดํ๋ ์๋น์ค ์ฝ๋ ์ญ์ ์ธํฐํ์ด์ค๋ฅผ ์์กดํ๋๋ก ๋ณ๊ฒฝํด์ฃผ์.
@Service
class OrderService(
private val checkStockPort: CheckStockPort,
) {
fun order(productId: Long) {
val result = checkStockPort.check(productId)
...
}
}
๊ทธ๋ฆฌ๊ณ , ๋ค์ ํ ์คํธ ์ฝ๋๋ก ๋์๊ฐ์ ‘test’๋ผ๋ profile์ ๊ฐ์ง ์ ์๋๋ก @ActiveProfiles ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํด์ฃผ๋๋ก ํ์.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ActiveProfiles("test")
class OrderControllerTestV2 {
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(2L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}
}
์ด๋ฌ๋ฉด @MockBean์ ์ฌ์ฉํ์ง ์๋๋ผ๋ ๊น๋ํ๊ฒ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ผ๋ฉฐ, ํ
์คํธ ์๋๋ฆฌ์ค์ ๋ฐ๋ผ์ ํ
์คํธ์ฉ ๋น์ด ์ด๋ค ํ์๋ฅผ ์ํํ ์ง ์ค์ ํด์ค๋ค๋ฉด ๋์ผํ ์ปจํ
์คํธ๋ฅผ ๊ฐ์ง๋๋ก ์ฌ์ฌ์ฉ๋ ํ ์ ์๊ธฐ ๋๋ฌธ์ ํ
์คํธ ์ฑ๋ฅ ์ญ์ ์ฌ๋ฆด ์ ์๋ค.
๐ฑ @TestConfiguration ํ์ฉํด์ฃผ๊ธฐ
์์ ๊ฐ์ด Profile ์ด๋ ธํ ์ด์ ์ ํ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง, @TestConfiguration ์ ํ์ฉํด์๋ ๋ค๋ฅธ ๋น์ ์ฌ์ฉํ๋๋ก ์ ์ดํ ์ ์๊ฒ ๋๋ค. ๋ง์ฝ, ์ด์ ์ฝ๋์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ ํ ๋ถ๋ฆฌํ๊ณ ์ถ๋ค๋ฉด ํ ์คํธ์ฉ ๋น์ ๋ฑ๋กํด์ฃผ๋๋ก ํ์.
๊ธฐ์กด์ ์ฝ๋์ ๊ฒฝ์ฐ ์ด์ ์ฝ๋์์ ์ปดํฌ๋ํธ ์ค์บ์ผ๋ก ์ธํด์ `TestStockCheckApi` ๊ฐ ๋น์ผ๋ก ๋ฑ๋ก์ด ๋์์๋๋ฐ, ํ ์คํธ ์ปจํ ์คํธ ๋ด์์๋ง ๋น์ผ๋ก ๋ฑ๋กํ๋๋ก ๋ง๋ค์ด๋ณด์. ๋จผ์ , @TestConfiguration์ ํ์ฉํ์ฌ ํ ์คํธ์ฉ ๋น์ ์ ์ธํด์ฃผ์.
@TestConfiguration
class TestConfig {
@Bean
fun checkStockPort() : CheckStockPort {
return TestStockCheckApi()
}
}
class TestStockCheckApi: CheckStockPort {
override fun check(productId: Long): StockStatus {
if (productId == 1L) {
return StockStatus.SUCCESS
}
return StockStatus.FAIL
}
}
๋ง์ฝ ๋์ผํ ํ์
์ ๋น์ ๋ฑ๋กํด์ค ์ผ์ด ์๋ค๋ฉด @Primary๋ฅผ ํ์ฉํ์ฌ ์ ์ ํ๊ฒ ์กฐ์ ํ์.
๊ทธ๋ฆฌ๊ณ , ํ ์คํธ ์ฝ๋์์ @Import๋ฅผ ํตํด์ (ํน์ @ContextConfiguration์ ํ์ฉํด๋ ๋๋ค) ํด๋น Configuration ์ ๋ํด ๋ก๋๋ฅผ ํด์ค๋ค.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
//@ContextConfiguration(classes = [TestConfig::class])
@Import(TestConfig::class)
class OrderControllerTestV3 {
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(2L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}
}
์ด๋ ๊ฒ ํ๋ฉด @Profile ์ด๋
ธํ
์ด์
๊ณผ ๋์ผํ๊ฒ ํ
์คํธ์ฉ checkStockPort ์ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
โ๏ธ @ConditionalOnProperty ํ์ฉํ๊ธฐ
๋ํ, @TestConfiguration๊ณผ @ConditionalOnProperty 2๊ฐ์ง์ ์ด๋
ธํ
์ด์
์ ์กฐํฉํด์ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ๊ตฌ์ฑํด๋ณผ ์๋ ์๋ค. ๋์ ์กฐ๊ธ ๋ ๋ณต์ก๋๊ฐ ๋์์ง๊ธฐ ๋๋ฌธ์ ๊ทธ๋ฅ ์ด๋ฐ ๋ฐฉ๋ฒ๋ ์๋ค๋ ์ ๋๋ง ๋์ด๊ฐ๋ ๋ ๊ฒ ๊ฐ๋ค.
๋จผ์ , ์ด์ ์ฝ๋์์ ๋ค์๊ณผ ๊ฐ์ด `@ConditionalOnProperty` ๋ฅผ ํ์ฉํ์ฌ ํน์ ํ๋กํผํฐ๊ฐ ์กด์ฌํ ๋ ํด๋น ๋น์ ์ฌ์ฉํ ์ ์๋๋ก ์์ ํด์ฃผ์.
@Component
@ConditionalOnProperty(name = ["check-stock.test"], havingValue = "false")
class ExternalStockCheckApi: CheckStockPort {
override fun check(productId: Long): StockStatus {
return callExternalApi(productId)
}
}
์ฌ๊ธฐ์์๋ `check-stock.test` ๋ผ๋ ํ๋กํผํฐ๊ฐ false์ธ ๊ฒฝ์ฐ์ ์ด์ ์ฝ๋๊ฐ ๋์ํ ์ ์๋๋ก ์ค์ ํด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ , ํ ์คํธ ์ฝ๋๋ฅผ ์กฐ๊ธ ์์ ํด์ฃผ์. ์ฐ์ , ํ๋กํผํฐ ๊ฐ์ ์ฌ์ฉํ๋ ๊น์ ํด๋น ํ๋กํผํฐ ๊ฐ์ ๋ฐ๋ผ์ success๋ฅผ ์๋ตํ๋ ์ธ๋ถ API์ fail์ ์๋ตํ๋ ์ธ๋ถ API ๋น์ผ๋ก ๋ถ๋ฆฌํ๊ณ ์ ํ๋ค. ์ด๋ฅผ ์ํด Conditional ์ธํฐํ์ด์ค๋ฅผ ํ์ฉํด์ค ์ ์๋ค.
class CheckStockSuccessCondition : Condition {
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
val environment = context.environment
val checkStockTest = environment.getProperty("check-stock.test") == "true"
val checkStockResultSuccess = environment.getProperty("check-stock.result-success") == "true"
return checkStockTest && checkStockResultSuccess
}
}
class CheckStockFailCondition : Condition {
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
val environment = context.environment
val checkStockTest = environment.getProperty("check-stock.test") == "true"
val checkStockResultFailure = environment.getProperty("check-stock.result-success") == "false"
return checkStockTest && checkStockResultFailure
}
}
`check-stock.test` ํ๋กํผํฐ๊ฐ true์ด๋ฉด์, `check-stock.result-success` ํ๋กํผํฐ๊ฐ true์ธ์ง false์ธ์ง์ ๋ฐ๋ผ์ Condition์ ๊ฒ์ฆํ๋ ๋ก์ง์ด๋ค. ๊ทธ๋ฆฌ๊ณ , ๋ค์๊ณผ ๊ฐ์ด ๋น์ ๋ฑ๋กํด์ฃผ์.
@TestConfiguration
class TestConfigV2 {
@Bean(name = ["checkStockPort"])
@Conditional(CheckStockSuccessCondition::class)
fun checkStockPortReturnSuccess(
environment: Environment
): CheckStockPort {
return object : CheckStockPort {
override fun check(productId: Long): StockStatus {
return StockStatus.SUCCESS
}
}
}
@Bean(name = ["checkStockPort"])
@Conditional(CheckStockFailCondition::class)
fun checkStockPortReturnFail(
environment: Environment
): CheckStockPort {
return object : CheckStockPort {
override fun check(productId: Long): StockStatus {
return StockStatus.FAIL
}
}
}
}
๋ ๋ค ‘checkStockPort’ ๋ผ๋ ์ด๋ฆ์ผ๋ก ๋น ๋ฑ๋ก์ด ๋ ์ ์๊ฒ ๋ค์ ์ง์ ์ ํด์ฃผ๊ณ , ๊ฐ๊ฐ์ ์กฐ๊ฑด์ด true์ผ ๋ ํด๋น ๋น์ ํ์ฉํ ์ ์๋๋ก @Conditional ์ด๋
ธํ
์ด์
์ ํ์ฉํด์ฃผ์๋ค.
๋ง์ง๋ง์ผ๋ก, ํ ์คํธ์์ ํด๋น ๋น ์ ๋ณด๋ฅผ ํ์ฉํ ์ ์๋๋ก import ํ๊ณ , ํ๋กํผํฐ๋ฅผ ์ฃผ์ ํด์ฃผ์. ํ๋กํผํฐ ์ฃผ์ ์ ์ํด์ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์๊ฒ ์ง๋ง, ๋๋ @SpringBootTest๊ฐ ์ ๊ณตํ๋ ํ๋กํผํฐ ์์ฑ์ ํ์ฉํ์ฌ ์ฃผ์ ํด์ฃผ์๋ค.
๋ค๋ง, ์ด๋ฌ๋ค ๋ณด๋๊น ์คํจ์ ์ฑ๊ณต ์ผ์ด์ค์ ๋ํด ๊ฐ๊ฐ ํ
์คํธ ํด๋์ค๋ฅผ ๋ถ๋ฆฌํ์๋ค. ์ด๋ฐ ๋ฐฉ๋ฒ๋ ์์ ๋ฟ์ด์ง, ์ค์ ๋ก ํ์ฉํ๊ธฐ์๋ ํจ์ฉ์ด ์ข์์ง๋ ์ ๋ชจ๋ฅด๊ฒ ๋ค. (์ ๋ง ์ด๋ฐ ๋ฐฉ๋ฒ์ด ์๊ธฐ๋ง ํ๋ค๋ ๊ฒ ์ ๋๋ง ์ธ์งํ์!)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = ["check-stock.test=true", "check-stock.result-success=true"]
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV2::class)
class OrderControllerTestV4_success {
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = ["check-stock.test=true", "check-stock.result-success=false"]
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV2::class)
class OrderControllerTestV4_fail {
...
}
๐ฑ mock ๊ฐ์ฒด๋ฅผ ํ์ฉํ๊ธฐ
๋ค์์ผ๋ก, ๋ง์ฝ ํ ์คํธ ์ํฉ์ ๋ฐ๋ผ ์ฌ๋ฌ ์๋๋ฆฌ์ค๋ฅผ ๋ฐํํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์๋ค.
์ด๋๋ ํ ์คํธ์ฉ ๋น์ ๋ฑ๋กํ ๋ ํ ์คํธ ํด๋์ค๊ฐ ์๋, mock ๊ฐ์ฒด๋ฅผ ๋ฐํํด์ฃผ๋ฉด ๋๋ค.
@TestConfiguration
class TestConfigV3 {
@Bean
fun checkStockPort() : CheckStockPort {
return Mockito.mock(CheckStockPort::class.java)
}
}
๋ง์ฝ kotest๋ฅผ ํ์ฉํ๋ค๋ฉด mockk<CheckStockPort> ๋ฅผ ํ์ฉํ์ฌ ๋ฐํํด์ค ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ , ํ
์คํธ ์ฝ๋์์ checkStockPort๋ฅผ ์ฃผ์
ํด์ฃผ์. ์ด๋ฌ๋ฉด ์์์ ์ ์ธํ mock ๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ค.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@Import(TestConfigV3::class)
class OrderControllerTestV5(
private val checkStockPort: CheckStockPort,
) {
}
์ด ์ํฉ์์ ๊ทธ๋ฅ ํ
์คํธ๋ฅผ ๋๋ฆฌ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
mocking๋ ๊ฐ์ฒด์ ๋ํด์ ํ๋ ์ผ์ ์ง์ ํด์ฃผ์ง ์์๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ๋ก null์ ๋ฐํํ๊ฒ ๋๋ฉฐ, ๋น์ฐํ๊ฒ๋ ํ
์คํธ๋ ์คํจํ๋ค.
์ด๋ฅผ ์ํด, ๊ฐ ํ
์คํธ ์๋๋ฆฌ์ค์ ๋ง์ถฐ ์ด๋ค ํ์๋ฅผ ํ ์ง ์ง์ ํด์ฃผ๋๋ก ํ์.
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์ ์ ์๋ต์ ๋ฐํํ๋ค`() {
Mockito.`when`(
checkStockPort.check(Mockito.anyLong())
).thenReturn(StockStatus.SUCCESS)
val result = callOrderApi(1L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.OK.value())
}
@Test
fun `์ฌ๊ณ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค`() {
Mockito.`when`(
checkStockPort.check(Mockito.anyLong())
).thenReturn(StockStatus.FAIL)
val result = callOrderApi(2L)
assertThat(result.statusCode())
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value())
}
Mockito์ When, ThenReturn ์ ์ ํ์ฉํ๋ฉด ์ด๋ค ํ์๋ฅผ ํ ์ง ์ง์ ํด์ค ์ ์์ผ๋ฉฐ, ๋ง์ฝ kotest๋ฅผ ์ฌ์ฉ ์ค์ด๋ผ๋ฉด every - returns๋ฅผ ์ฌ์ฉํ์ฌ ํด์ฃผ๋ฉด ๋๋ค.
๐ฑ ๋ง๋ฌด๋ฆฌ
์์ฆ ํ์ฌ์์ ํตํฉ ํ
์คํธ๋ฅผ ์์ฃผ ์ง๋ ค๊ณ ๋
ธ๋ ฅ ์ค์ธ๋ฐ, ํ ๋ฒ ๊ตฌ์ถํ๊ณ ๋๋๊น ๋ค์ ๋ฒ์ ํ
์คํธ๋ฅผ ์งค ๋ ๋ ๊ธ๋ฐฉ ์งค ์ ์๋ ๊ฒ ๊ฐ๋ค.
๊ฐ์ธ์ ์ผ๋ก๋ ์ 4๊ฐ์ง ๋ฐฉ๋ฒ ์ค์์ 2๋ฒ๊ณผ 4๋ฒ์ ๋ง์ด ์ฐ๊ณ ์๋ค.
2๋ฒ์ ์ฌ๋ ๋ฉ์์ง๋ ๊ธ์ ์ง๊ธ, ์ธ๋ถ API๋ฅผ ํธ์ถํด์ผ ํ๋ ์ํฉ ๋ฑ๋ฑ, ํ
์คํธ์์ ๋ฐ๋์ ํธ์ถ์ด ๋์ง ์์ ํ์๊ฐ ์๊ฑฐ๋ ๊ณ ์ ๋ ํน์ ํ ์๋ต์ด ํ์ํ ๊ฒฝ์ฐ ์ฌ์ฉํ๊ณ , 4๋ฒ์ ์ธ๋ถ API์ด์ง๋ง ํด๋น API๊ฐ ์ด๋ค ์๋ต์ ๋ด๋ ค์ฃผ๋์ง์ ๋ฐ๋ผ์ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ฌ๋ผ์ง๋ ๊ฒฝ์ฐ ์๋๋ฆฌ์ค๋ณ๋ก ์ ์ํ๊ธฐ ์ํด์ ์ฌ์ฉํ๊ณ ์๋ค.
์์ฆ ํ ์คํธ ์๋์ ๋ํด์๋ ๊ณ ๋ฏผ์ด ๋ง์์, ์์ ํ ์คํธ์ฉ ๋น์ ๊ด๋ฆฌํ๋ @TestConfiguration ํด๋์ค๋ฅผ ์์ฑํ ๋ค์์, ๋ชจ๋ ํด๋์ค์์ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ๋ง๋ค์ด์ ์๋๋ฅผ ๊ฐ์ ํด๋ณผ๊น๋ ๊ณ ๋ฏผ ์ค์ด๋ค. ๋ํ, ๊ฐ์ธ์ ์ผ๋ก๋ kotest๋ฅผ ์ ํธํ๋ ํธ์ด๋ผ์ mockk์ ํจ๊ป ์กฐํฉํด์ ๋ง์ด ์ฌ์ฉํ๊ณ ์๋ค.
์ค๋๋ง์ ๋ธ๋ก๊ทธ ๊ธ์ ์์ฑํด์ ์ด์ํ๋ฐ, ์ผ๋ฅธ ๋ค๋ฅธ ๊ธ๋ ์์ฑํ ์ ์๋๋ก ๋ ธ๋ ฅํด์ผ๊ฒ ๋ค... ๋!