Spy์ Mock์ ๊ฐ๋
๊ธฐ๋ณธ ๊ฐ๋
Spy์ Mock์ ํ ์คํธ์์ ์ค์ ๊ฐ์ฒด๋ฅผ ๋์ฒดํ๋ Test Double์ ํ ์ข ๋ฅ์ด๋ค. ์ค์ํ์ ๋น์ ํ์๋ฉด, ์ํ ์ดฌ์์์ ์ํํ ์ฅ๋ฉด์ ๋์ ํ๋ ์คํดํธ๋งจ๊ณผ ๊ฐ์ ์ญํ ์ ํ๋ค. test double์ ์ข ๋ฅ
- ๋ฏธ๋ฆฌ ์ ์๋ ๋์์ ์ํํ๋ ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค
- ํน์ ๋ฉ์๋ ํธ์ถ์ ๊ธฐ๋ํ๊ณ ๊ทธ์ ๋ํ ๋ฐํ๊ฐ์ ์ ์ํ๋ค
Spy
- ์ค์ ๊ฐ์ฒด์ ํ๋์ ๊ด์ฐฐํ๊ณ ๊ธฐ๋กํ๋ค
- ๋ฉ์๋ ํธ์ถ ์ฌ๋ถ, ํ์, ์ ๋ฌ๋ ํ๋ผ๋ฏธํฐ ๋ฑ์ ๊ฒ์ฆํ๋ค
๊ธฐ๋ณธ ๋์ ๋ฐฉ์
Mock ๊ฐ์ฒด ์์ฑ ๋ฐ ์ฌ์ฉ
// PaymentGateway ์ธํฐํ์ด์ค
interface PaymentGateway {
public function process(float $amount): bool;
public function refund(float $amount): bool;
}
// Mock ๊ฐ์ฒด ์์ฑ
public function testOrderPayment(): void
{
// Mock ๊ฐ์ฒด ์์ฑ
$mock = $this->mock(PaymentGateway::class);
// ๋์ ์ ์
$mock->shouldReceive('process')
->with(100.00)
->once()
->andReturn(true);
// ํ
์คํธ ๋์ ์ฝ๋ ์คํ
$order = new Order($mock);
$result = $order->pay(100.00);
// ๊ฒฐ๊ณผ ๊ฒ์ฆ
$this->assertTrue($result);
}Spy ๊ฐ์ฒด ์์ฑ ๋ฐ ์ฌ์ฉ
// NotificationService ํด๋์ค
class NotificationService {
public function sendEmail(string $to, string $subject): void
{
// ์ค์ ์ด๋ฉ์ผ ๋ฐ์ก ๋ก์ง
}
}
// Spy ๊ฐ์ฒด๋ฅผ ํ์ฉํ ํ
์คํธ
public function testUserRegistration(): void
{
// Spy ๊ฐ์ฒด ์์ฑ
$spy = Mockery::spy(NotificationService::class);
// ์์กด์ฑ ์ฃผ์
$this->app->instance(NotificationService::class, $spy);
// ํ
์คํธ ๋์ ์ฝ๋ ์คํ
$user = User::factory()->create();
// ๋ฉ์๋ ํธ์ถ ๊ฒ์ฆ
$spy->shouldHaveReceived('sendEmail')
->with($user->email, '๊ฐ์
์ ํ์ํฉ๋๋ค')
->once();
}Process Flow
sequenceDiagram participant TestCode as ํ ์คํธ ์ฝ๋ participant Mock as Mock/Spy ๊ฐ์ฒด participant Target as ํ ์คํธ ๋์ TestCode->>Mock: 1. ๊ฐ์ฒด ์์ฑ TestCode->>Mock: 2. ๋์ ์ ์ TestCode->>Target: 3. ํ ์คํธ ์คํ Target->>Mock: 4. ๋ฉ์๋ ํธ์ถ Mock-->>Target: 5. ์ ์๋ ์๋ต ๋ฐํ TestCode->>Mock: 6. ํธ์ถ ๊ฒ์ฆ
์ค์ ์ฌ์ฉ ์์
1. API ํธ์ถ Mock ์ฒ๋ฆฌ
public function testProductSync(): void
{
// HTTP ํด๋ผ์ด์ธํธ Mock
$mock = $this->mock(HttpClient::class);
// API ์๋ต ์ ์
$mock->shouldReceive('get')
->with('https://api.example.com/products')
->once()
->andReturn([
'products' => [
['id' => 1, 'name' => '์ํA'],
['id' => 2, 'name' => '์ํB']
]
]);
// ๋๊ธฐํ ์คํ
$syncer = new ProductSyncer($mock);
$result = $syncer->sync();
// ๊ฒฐ๊ณผ ๊ฒ์ฆ
$this->assertEquals(2, Product::count());
}2. Event ๋ฐ์ ๊ฒ์ฆ
public function testOrderCompletion(): void
{
// Event Spy ์ค์
Event::spy();
// ์ฃผ๋ฌธ ์๋ฃ ์ฒ๋ฆฌ
$order = Order::factory()->create();
$order->complete();
// ์ด๋ฒคํธ ๋ฐ์ ๊ฒ์ฆ
Event::assertDispatched(OrderCompletedEvent::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
}๊ณ ๊ธ ํ์ฉ๋ฒ
1. ์์ฐจ์ ์๋ต ์ ์
// ์ฌ๋ฌ ๋ฒ์ ํธ์ถ์ ๋ํด ๋ค๋ฅธ ์๋ต์ ์ ์
$mock->shouldReceive('process')
->andReturn(true, false, true) // ์ฒซ ๋ฒ์งธ, ๋ ๋ฒ์งธ, ์ธ ๋ฒ์งธ ํธ์ถ์ ์๋ต
->times(3);2. ๋์ ์๋ต ์ฒ๋ฆฌ
$mock->shouldReceive('calculate')
->andReturnUsing(function ($amount) {
return $amount * 1.1; // 10% ํ ์ฆ ๊ณ์ฐ
});์ฃผ์์ฌํญ
1. Mock ์ฌ์ฉ ์ ์ฃผ์์
- Mock์ ์ค์ ๊ฐ์ฒด์ ๋์์ ์์ ํ ๋์ฒดํ๋ฏ๋ก, ๊ณผ๋ํ ์ฌ์ฉ์ ํ ์คํธ์ ์ ๋ขฐ์ฑ์ ์ ํ์ํจ๋ค
- ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ๊ฐ๋ฅํ ์ค์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ฌ ํ ์คํธํ๋ค
2. Spy ์ฌ์ฉ ์ ์ฃผ์์
// ์๋ชป๋ ์์
$spy->shouldReceive('process') // Spy๋ ๋์์ ๋ฏธ๋ฆฌ ์ ์ํ์ง ์๋๋ค
->andReturn(true);
// ์ฌ๋ฐ๋ฅธ ์์
$spy->shouldHaveReceived('process') // ํธ์ถ ์ฌ๋ถ๋ง ๊ฒ์ฆํ๋ค
->once();Security ๊ณ ๋ ค์ฌํญ
1. ๋ฏผ๊ฐํ ์ ๋ณด ์ฒ๋ฆฌ
// ์๋ชป๋ ์์
$mock->shouldReceive('getApiKey')
->andReturn('actual-secret-key'); // ์ค์ ํค๋ฅผ ํ
์คํธ ์ฝ๋์ ํฌํจ์ํค์ง ์๋๋ค
// ์ฌ๋ฐ๋ฅธ ์์
$mock->shouldReceive('getApiKey')
->andReturn(Str::random(32)); // ๊ฐ์ง ๋ฐ์ดํฐ ์ฌ์ฉPerformance ๊ณ ๋ ค์ฌํญ
1. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
public function tearDown(): void
{
Mockery::close(); // Mock ๊ฐ์ฒด ์ ๋ฆฌ
parent::tearDown();
}๊ฒฐ๋ก
Spy์ Mock์ Laravel ํ ์คํธ์์ ๊ฐ๋ ฅํ ๋๊ตฌ์ด์ง๋ง, ์ ์ ํ ์ฌ์ฉ์ด ์ค์ํ๋ค. ๋ค์ ์ฌํญ์ ๊ณ ๋ คํ์ฌ ์ฌ์ฉํ๋ค:
- Mock์ ์ธ๋ถ ์์กด์ฑ์ ๋์ฒดํ ๋ ์ฌ์ฉํ๋ค
- Spy๋ ๊ฐ์ฒด์ ํ๋์ ๊ด์ฐฐํ ๋ ์ฌ์ฉํ๋ค
- ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ์ค์ ๊ฐ์ฒด๋ก ํ ์คํธํ๋ค
- ํ ์คํธ์ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๊ณ ๋ คํ๋ค
์ด๋ฌํ ๋๊ตฌ๋ค์ ์ ์ ํ ํ์ฉํ๋ฉด ์์ ์ ์ด๊ณ ํจ์จ์ ์ธ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.