모킹
업데이트됨번역일: 2026년 9월 30일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 30일
- 번역 갱신
- 2026년 9월 30일
모킹
소개
Laravel 애플리케이션을 테스트하다 보면 애플리케이션의 특정 부분이 실제로 실행되지 않도록 "모킹(mocking)"해야 할 때가 있습니다. 예를 들어 이벤트를 발생시키는 컨트롤러를 테스트할 때, 이벤트 리스너가 실제로 실행되지 않도록 모킹하고 싶을 수 있습니다. 이렇게 하면 이벤트 리스너의 실행 여부는 신경 쓰지 않고 컨트롤러의 HTTP 응답만 테스트할 수 있으며, 이벤트 리스너는 별도의 테스트 케이스에서 검증하면 됩니다.
Laravel은 이벤트, Job, 그 밖의 여러 파사드를 손쉽게 모킹할 수 있는 메서드를 기본으로 제공합니다. 이 헬퍼들은 사실 Mockery 위에 편의 계층을 얹은 것으로, 복잡한 Mockery 메서드 호출을 직접 작성하지 않아도 되도록 도와줍니다.
객체 모킹
Laravel의 서비스 컨테이너를 통해 애플리케이션에 주입되는 객체를 모킹하려면, 모킹한 인스턴스를 instance 바인딩으로 컨테이너에 등록해야 합니다. 이렇게 하면 컨테이너는 실제 객체를 생성하는 대신 여러분이 등록한 모킹 인스턴스를 사용하게 됩니다.
Pest
use App\Service;
use Mockery;
use Mockery\MockInterface;
test('something can be mocked', function () {
$this->instance(
Service::class,
Mockery::mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
})
);
});PHPUnit
use App\Service;
use Mockery;
use Mockery\MockInterface;
public function test_something_can_be_mocked(): void
{
$this->instance(
Service::class,
Mockery::mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
})
);
}이를 좀 더 편리하게 사용하려면 Laravel의 기본 테스트 케이스 클래스가 제공하는 mock 메서드를 사용하면 됩니다. 다음 예시는 위 코드와 동일하게 동작합니다.
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});객체의 일부 메서드만 모킹하고 싶다면 partialMock 메서드를 사용할 수 있습니다. 모킹하지 않은 메서드는 호출 시 평소처럼 정상적으로 실행됩니다.
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});마찬가지로 객체를 스파이(spy)로 만들고 싶다면, Laravel의 기본 테스트 케이스 클래스가 제공하는 spy 메서드를 사용할 수 있습니다. 이는 Mockery::spy 메서드를 감싼 편의 래퍼입니다. 스파이는 모킹 객체와 비슷하지만, 스파이는 테스트 대상 코드와 스파이 사이에 일어난 모든 상호작용을 기록해 두었다가 코드 실행 후에 이를 검증(assertion)할 수 있게 해줍니다.
use App\Service;
$spy = $this->spy(Service::class);
// ...
$spy->shouldHaveReceived('process');파사드 모킹
일반적인 정적 메서드 호출과 달리 파사드(그리고 실시간 파사드 포함)는 모킹이 가능합니다. 이는 전통적인 정적 메서드 방식에 비해 큰 장점이며, 의존성 주입을 사용할 때와 동일한 수준의 테스트 용이성을 제공합니다. 테스트를 작성할 때 컨트롤러 안에서 호출되는 Laravel 파사드를 모킹하고 싶은 경우가 많습니다. 다음 컨트롤러 액션을 예로 들어보겠습니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* 애플리케이션의 전체 사용자 목록을 조회합니다.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}expects 메서드를 사용하면 Cache 파사드 호출을 모킹할 수 있으며, 이 메서드는 Mockery 모킹 인스턴스를 반환합니다. 파사드는 실제로 Laravel 서비스 컨테이너를 통해 해석(resolve)되고 관리되기 때문에, 일반적인 정적 클래스보다 테스트하기가 훨씬 수월합니다. 예를 들어 Cache 파사드의 get 메서드 호출을 모킹해보겠습니다.
Pest
<?php
use Illuminate\Support\Facades\Cache;
test('get index', function () {
Cache::expects('get')
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
});PHPUnit
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
public function test_get_index(): void
{
Cache::expects('get')
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
}
}WARNING
Request 파사드는 모킹하지 않는 것이 좋습니다. 대신 테스트를 실행할 때 get, post와 같은 HTTP 테스트 메서드에 원하는 입력값을 직접 전달하세요. 마찬가지로 Config 파사드를 모킹하는 대신, 테스트 안에서 Config::set 메서드를 호출해 값을 설정하세요.
파사드 스파이
파사드를 스파이로 만들고 싶다면 해당 파사드의 spy 메서드를 호출하면 됩니다. 스파이는 모킹 객체와 비슷하지만, 테스트 대상 코드와 스파이 사이의 모든 상호작용을 기록해 두었다가 코드 실행 후에 검증할 수 있게 해줍니다.
Pest
<?php
use Illuminate\Support\Facades\Cache;
test('values are stored in cache', function () {
Cache::spy();
$response = $this->get('/');
$response->assertStatus(200);
Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
});PHPUnit
use Illuminate\Support\Facades\Cache;
public function test_values_are_stored_in_cache(): void
{
Cache::spy();
$response = $this->get('/');
$response->assertStatus(200);
Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
}시간 다루기
테스트를 작성하다 보면 now나 Illuminate\Support\Carbon::now() 같은 헬퍼가 반환하는 시간 값을 강제로 바꿔야 할 때가 있습니다. Laravel의 기본 기능 테스트 클래스는 현재 시간을 자유롭게 조작할 수 있는 헬퍼 메서드를 제공합니다.
Pest
test('time can be manipulated', function () {
// 미래로 시간 이동...
$this->travel(5)->milliseconds();
$this->travel(5)->seconds();
$this->travel(5)->minutes();
$this->travel(5)->hours();
$this->travel(5)->days();
$this->travel(5)->weeks();
$this->travel(5)->years();
// 과거로 시간 이동...
$this->travel(-5)->hours();
// 특정 시각으로 이동...
$this->travelTo(now()->minus(hours: 6));
// 현재 시각으로 복귀...
$this->travelBack();
});PHPUnit
public function test_time_can_be_manipulated(): void
{
// 미래로 시간 이동...
$this->travel(5)->milliseconds();
$this->travel(5)->seconds();
$this->travel(5)->minutes();
$this->travel(5)->hours();
$this->travel(5)->days();
$this->travel(5)->weeks();
$this->travel(5)->years();
// 과거로 시간 이동...
$this->travel(-5)->hours();
// 특정 시각으로 이동...
$this->travelTo(now()->minus(hours: 6));
// 현재 시각으로 복귀...
$this->travelBack();
}각 시간 이동 메서드에는 클로저를 전달할 수도 있습니다. 이 경우 지정한 시각으로 시간이 고정된 상태에서 클로저가 실행되며, 클로저 실행이 끝나면 시간은 다시 정상적으로 흐릅니다.
$this->travel(5)->days(function () {
// 5일 후 시점의 동작을 테스트...
});
$this->travelTo(now()->minus(days: 10), function () {
// 특정 시점의 동작을 테스트...
});freezeTime 메서드를 사용하면 현재 시각을 그대로 고정할 수 있습니다. 이와 비슷하게 freezeSecond 메서드는 현재 시각을 초 단위로 (해당 초의 시작 시점에) 고정합니다.
use Illuminate\Support\Carbon;
// 시간을 고정하고, 클로저 실행 후 다시 정상적으로 흐르게 함...
$this->freezeTime(function (Carbon $time) {
// ...
});
// 현재 초로 시간을 고정하고, 클로저 실행 후 다시 정상적으로 흐르게 함...
$this->freezeSecond(function (Carbon $time) {
// ...
});짐작하셨겠지만, 위에서 살펴본 메서드들은 시간에 민감한 애플리케이션 동작을 테스트할 때 특히 유용합니다. 예를 들어 게시판에서 일정 기간 활동이 없는 게시글을 자동으로 잠그는 기능을 테스트하는 경우를 살펴보겠습니다.
Pest
use App\Models\Thread;
test('forum threads lock after one week of inactivity', function () {
$thread = Thread::factory()->create();
$this->travel(1)->week();
expect($thread->isLockedByInactivity())->toBeTrue();
});PHPUnit
use App\Models\Thread;
public function test_forum_threads_lock_after_one_week_of_inactivity()
{
$thread = Thread::factory()->create();
$this->travel(1)->week();
$this->assertTrue($thread->isLockedByInactivity());
}