matcher ๊ฐ๋
matcher = โ์ธ์ ๋งค์นญ ๊ท์นโ
any()= ์๋ฌด ๊ฐ์ด๋ฉด ๋งค์นeq(x)= equals ๊ธฐ์ค์ผ๋ก x์ ๊ฐ์ผ๋ฉด ๋งค์นisNull()= null์ด๋ฉด ๋งค์นargThat { ... }= ์ฌ์ฉ์ ์ ์ ์กฐ๊ฑด์ด๋ฉด ๋งค์น
ํต์ฌ ๋์: โ์คํ์ ๊ธฐ๋ก + ๋๋ฏธ๊ฐ(dummy value) ๋ฐํโ
Mockito ๊ณต์ Javadoc์ matcher๊ฐ ์ด๋ ๊ฒ ๋์ํ๋ค๊ณ ๋ช ์ํ๋ค.
any(),eq()๋ matcher๋ฅผ ๋ฐํํ๋ ๊ฒ ์๋๋ผ- ๋งค์นญ ๊ท์น์ ๋ด๋ถ ์คํ์ ๊ธฐ๋กํ๊ณ
- ์ปดํ์ผ๋ฌ ํ์ ์๊ตฌ ๋๋ฌธ์ ๋๋ฏธ ๊ฐ(๋๊ฐ null)์ ๋ฐํํ๋ค
- ๊ทธ๋์
any(),eq()๊ฐ์ matcher๋ verified/stubbed method ๋ฐ๊นฅ์์ ์ฌ์ฉํ๋ฉด ์ ๋๋ค
Q) โmatcher๋ก ํ์๋ ๊ฐ์ ํ ์คํธํ ๋ ๊ท์น์ ๋ง๋ ๋๋ค๊ฐ์ด ๋ค์ด๊ฐ๋ค๊ณ ๋ณด๋ฉด ๋๋?โ
- ์๋๋ค. matcher๊ฐ ๊ฐ์ โ์์ฑโํ์ง ์๋๋ค.
- SUT๊ฐ ์ค์ ๋ก ์ ๋ฌํ ๊ฐ์ด ๋ค์ด์ค๊ณ , matcher๋ ๊ทธ ๊ฐ์ด ๊ท์น์ ๋ง๋์ง ํ์ ํ๋ค.
- ๋จ, matcher๋ฅผ ์ค์๋ก SUT ํธ์ถ ์ธ์์ ๋ฃ์ผ๋ฉด matcher๊ฐ ๋ฐํํ ๋๋ฏธ๊ฐ(null ๋ฑ) ์ด ์ค์ ์ธ์๋ก ๋ค์ด๊ฐ ํ๋ฆ์ด ๋ฌ๋ผ์ง ์ ์๋ค.
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- ์ ์ฐํ ์ธ์ ์กฐ๊ฑด์ ํํํ๊ธฐ ์ํด(๊ฐ์ด ์๋๋ผ ๊ท์น)
- ๋ณ๋ ๊ฐ(UUIDยท์๊ฐ ๋ฑ) ๋๋ฌธ์ ํ ์คํธ๊ฐ ์์ฃผ ๊นจ์ง๋ ๊ฑธ ์ค์ด๊ธฐ ์ํด
- ์ค์ํ ์ธ์๋ง ์ ํํ ๊ฒ์ฆํ๊ณ ๋๋จธ์ง๋ ๋จ์ํํด ์๋๋ฅผ ๋ช ํํ ํ๊ธฐ ์ํด
AS-IS / TO-BE ์์
์: ๊ฒฐ์ ์์ฒญ์ ๋ณด๋ด๋ ๋ก์ง์์ requestId๋ ๋งค๋ฒ ๋ฐ๋๊ณ , userId/amount๋ง ์ ํํ ๊ฒ์ฆํ๊ณ ์ถ๋ค.
data class PayRequest(val userId: String, val requestId: String, val amount: Int)
interface PaymentGateway {
fun send(req: PayRequest)
}
class PaymentService(private val gateway: PaymentGateway) {
fun pay(userId: String, amount: Int) {
val req = PayRequest(
userId = userId,
requestId = java.util.UUID.randomUUID().toString(), // ๋งค๋ฒ ๋ฐ๋
amount = amount
)
gateway.send(req)
}
}AS-IS (matcher๊ฐ ์์ ๋)
@Test
fun `pay sends request`() {
val gateway = mock<PaymentGateway>()
val sut = PaymentService(gateway)
sut.pay("u-1", 1000)
// โ requestId๊ฐ ๋งค๋ฒ ๋ฌ๋ผ์ ๊ธฐ๋๊ฐ์ ๊ณ ์ ํ๊ธฐ ์ด๋ ค์ โ ํ
์คํธ๊ฐ ์ทจ์ฝํด์ง
verify(gateway).send(PayRequest("u-1", "๊ณ ์ ๊ฐ-๋ถ๊ฐ๋ฅ", 1000))
}TO-BE (matcher ๋์ ํ)
@Test
fun `pay sends request - rule based`() {
val gateway = mock<PaymentGateway>()
val sut = PaymentService(gateway)
sut.pay("u-1", 1000)
// โ
requestId๋ "์๊ธฐ๋ง ํ๋ฉด ๋จ", userId/amount๋ ์ ํํ ํ์ธ
verify(gateway).send(
argThat { userId == "u-1" && amount == 1000 && requestId.isNotBlank() }
)
}flowchart TD A[ํ ์คํธ์์ ์ธ์ ๊ฒ์ฆ ํ์] --> B{์ธ์๊ฐ ํญ์ ๊ณ ์ ๊ฐ์ธ๊ฐ?} B -- Yes --> C[raw ๊ฐ์ผ๋ก verify/stub ๊ฐ๋ฅ] B -- No --> D[AS-IS: ๊ฐ ๊ณ ์ ์ด๋ ค์ โ ํ ์คํธ ์ทจ์ฝ/์๋ฏธ ์ฝํ] D --> E[TO-BE: matcher๋ก '๊ท์น' ์ ์ธ] E --> H[์๋ ์ ๋ช + ๋ณ๊ฒฝ์ ๋ ๊นจ์ง]
matcher ๊ท์น 3๊ฐ
๊ท์น 1. matcher๋ฅผ ํ๋๋ผ๋ ์ฐ๋ฉด, ๋ชจ๋ ์ธ์๋ฅผ matcher๋ก ํต์ผ
Mockito Javadoc์ ๋ํ ๊ฒฝ๊ณ ๋ค.
// โ
OK
verify(gateway).send(anyInt(), anyString(), eq("third"))
// โ NG: matcher + raw ๊ฐ ํผ์ฉ โ ์์ธ
verify(gateway).send(anyInt(), anyString(), "third")Q) โ๊ทธ๋ผ eq๋ ์ ํํ ๊ฐ์ ๊ฐ์ธ๊ธฐ ์ํ ๊ท์น์ด๋ผ๊ณ ๋ณด๋ฉด ๋๋?โ
๋ง๋ค.eq(value)๋ โ์ ํํ ๊ฐ์ด์ด์ผ ํจโ์ matcher๋ก ํํํ๋ฉด์, โmatcher ํต์ผ ๊ท์นโ์ ์งํค๊ฒ ํด์ค๋ค.
๊ท์น 2. matcher๋ verify / stubbing ์์์๋ง
matcher๋ โ์คํ์ ๊ธฐ๋ก + ๋๋ฏธ๊ฐ ๋ฐํโ ๋ฐฉ์์ด๋ฏ๋ก, verified/stubbed method ๋ฐ๊นฅ(= SUT ํธ์ถ ์ธ์)์์ ์ฌ์ฉํ๋ฉด ์ ๋๋ค.
- โ
verify(mock).doSomething(any()) - โ
whenever(mock.load(eq("id"))).thenReturn(data) - โ
sut.process(any())โ SUT ํธ์ถ ์ธ์์ matcher ์ฌ์ฉ
๊ท์น 3. null์ โ์๋์ ์ผ๋กโ ๋งค์นญํ๋ค
any(Class) / anyInt ๊ฐ์ ํ์
์ฒดํฌ matcher๋ null์ ๋งค์นญํ์ง ์๋๋ค๋ ์ ์ด Javadoc์ ๋ช
์๋์ด ์๋ค. null์ด ํ์ํ๋ฉด isNull()์ ์ ํํ๋ ๊ฒ์ด ๊ฐ์ฅ ๋ช
ํํ๋ค.
doReturn)์์ matcher ์ฌ์ฉํ๋ ๋ฒ
whenever(...).thenReturn(...) ๊ธฐ๋ณธ ํํ
interface FeatureFlagClient {
fun isEnabled(key: String, userId: String): Boolean
}
val client = mock<FeatureFlagClient>()
// stubbing: ํน์ ์กฐ๊ฑด์ด๋ฉด true๋ฅผ ๋ฐํ
whenever(client.isEnabled(eq("NEW_UI"), eq("u-1"))).thenReturn(true)stubbing์์๋ โmatcher๋ฅผ ํ๋๋ผ๋ ์ฐ๋ฉด ์ ๋ถ matcherโ
์๋๋ stubbing์์ ํํ ๋ง๋๋ InvalidUseOfMatchersException ํจํด์ด๋ค. (๊ท์น #1 ์๋ฐ)
// โ raw + matcher ํผ์ฉ
whenever(client.isEnabled("NEW_UI", any())).thenReturn(true)
// โ
์ ๋ถ matcher๋ก ํต์ผ
whenever(client.isEnabled(eq("NEW_UI"), any())).thenReturn(true)3.3 null์ด ๋ค์ด์ฌ ์ ์์ผ๋ฉด isNull() ๋๋ nullable ํ์ฉ matcher๋ฅผ ์ ํ
Mockito ๊ด์ ์์ ๊ฐ์ฅ ๋ช
ํํ ์ ํ์ isNull()์ด๋ค.
interface TokenStore {
fun find(tokenId: String?): String?
}
val store = mock<TokenStore>()
whenever(store.find(isNull())).thenReturn("guest-token")doReturn(...).whenever(...)๋ ์ธ์ ์ฐ๋?
Mockito๋ doReturn() | doThrow() | doAnswer() ... ๊ฐ์ do-ํจ๋ฐ๋ฆฌ API๋ฅผ ์ ๊ณตํ๋ค.
์ค๋ฌด ํ(ํนํ spy ์ํฉ):
whenever(spy.method())๋ spy์ ์ค์ ๋ฉ์๋๊ฐ ๋จผ์ ์คํ๋ ์ ์์ด ์ํํ ๊ฒฝ์ฐ๊ฐ ์๋ค.- ์ด๋ฐ ๊ฒฝ์ฐ
doReturn(x).whenever(spy).method(...)ํํ๋ก โ์ค์ ํธ์ถ ์์ดโ ์คํ ์ ๊ฑธ ์ ์๋ค.
val spyClient = spy(RealClient())
doReturn("cached").whenever(spyClient).fetch(eq("key"))์์ฃผ ๋์ค๋ ์๋ฌ ๋ก๊ทธ๋ฅผ โ๊ท์นโ์ผ๋ก ํ์ํ๊ธฐ
InvalidUseOfMatchersException: X matchers expected, Y recorded
์๋ฏธ(๊ฑฐ์ ํญ์): ๊ฐ์ ํธ์ถ์์ matcher์ raw ๊ฐ์ ์์๋ค.
ํด๊ฒฐ: raw ๊ฐ์ eq(...)๋ก ๊ฐ์ธ์ โ์ ๋ถ matcherโ๋ก ๋ง๋ค๊ฑฐ๋, ์ ๋ถ raw๋ก ๋ฐ๊พผ๋ค.
// โ
verify(gateway).send(any(), "tenantA")
// โ
verify(gateway).send(any(), eq("tenantA"))Argument(s) are different! Wanted ... Actual ...
์๋ฏธ: ํธ์ถ์ ์์๋๋ฐ, ์ธ์๊ฐ ๊ธฐ๋์ ๋ค๋ฅด๋ค.
์์ฃผ ๋์ค๋ ์์ธ:
- null์ด ์ค์ ๋ก ๋ค์ด์ค๋๋ฐ
any(Class)/primitive ๊ณ์ด๋ก ๊ฒ์ฆํด์ ์คํจ- ํด๊ฒฐ: null์ด๋ฉด
isNull()๋ก ์๋๋ฅผ ๋ช ํํ
- ํด๊ฒฐ: null์ด๋ฉด
verify(store).find(isNull())Wanted but not invoked (Actually, there were zero interactions with this mock)
์๋ฏธ: ํด๋น mock์ ๋ํด ์ํ๋ ํธ์ถ ์์ฒด๊ฐ ์์๋ค.
์ฐ์ ์์ ์ฒดํฌ๋ฆฌ์คํธ:
- SUT๊ฐ ํด๋น ๋ก์ง ๋ถ๊ธฐ/์ํ๋ฅผ ์ค์ ๋ก ํ๋์ง(์กฐ๊ฑด/์ ๋ ฅ/์ํ)
- stubbing์ด ๋งค์นญ ์คํจํด์ ์ค๊ฐ์ ํ๋ฆ์ด ๋๊ธด ๊ฑด ์๋์ง(ํนํ matcher ํผ์ฉ/nullable)
- ๋น๋๊ธฐ/์ฝ๋ฃจํด์ด๋ผ๋ฉด ํ ์คํธ ์ค์ผ์ค๋ฌ ๋ฐ ๋์คํจ์ฒ/์ค์ฝํ์์ ์คํ ์ค์ธ ๊ฑด ์๋์ง
- โ๊ฒ์ฆ ๋์โ mock ์ธ์คํด์ค๊ฐ ๋ง๋์ง(๋ค๋ฅธ ์ธ์คํด์ค๋ฅผ ์ฃผ์ /์์ฑํ ๊ฑด ์๋์ง)
์ต์ข ์์ฝ(์ด์ ๊ท์น 6์ค)
- matcher๋ โ๊ฐโ์ด ์๋๋ผ โํ์ ๊ท์นโ์ด๋ค.
- matcher๋ฅผ ํ๋๋ผ๋ ์ฐ๋ฉด ๋ชจ๋ ์ธ์๋ฅผ matcher๋ก ํต์ผํ๋ค.
- matcher๋ verify/stubbing ์์์๋ง ์ฌ์ฉํ๋ค. (SUT ํธ์ถ์๋ ์ค์ ๊ฐ๋ง)
- ๊ฐ ๊ณ ์ ์ด ํ์ํ๋ฉด
eq(value)๋ฅผ ์ด๋ค. (์ ํํ ๊ฐ๋ matcher๋ก ํํ) - null์
isNull()๋ก ์๋๋ฅผ ๋ช ํํ ํ๋ค. (any(Class)/primitive ๊ณ์ด matcher๋ null ๋ฏธ๋งค์นญ) - spy์์ ์ค์ ํธ์ถ์ ํผํด์ผ ํ๋ฉด
doReturn(...).whenever(...)๋ฅผ ๊ณ ๋ คํ๋ค.