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 ํ…Œ์ŠคํŠธ์—์„œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ด์ง€๋งŒ, ์ ์ ˆํ•œ ์‚ฌ์šฉ์ด ์ค‘์š”ํ•˜๋‹ค. ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค:

  1. Mock์€ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๋Œ€์ฒดํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค
  2. Spy๋Š” ๊ฐ์ฒด์˜ ํ–‰๋™์„ ๊ด€์ฐฐํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค
  3. ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ์‹ค์ œ ๊ฐ์ฒด๋กœ ํ…Œ์ŠคํŠธํ•œ๋‹ค
  4. ํ…Œ์ŠคํŠธ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๊ณ ๋ คํ•œ๋‹ค

์ด๋Ÿฌํ•œ ๋„๊ตฌ๋“ค์„ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด ์•ˆ์ •์ ์ด๊ณ  ํšจ์œจ์ ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.