모킹
업데이트됨번역일: 2026년 9월 17일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 17일
- 번역 갱신
- 2026년 9월 17일
모킹
소개
Laravel 애플리케이션을 테스트하다 보면, 특정 기능을 실제로 실행하지 않고 "모킹(mocking)"하고 싶을 때가 있습니다. 예를 들어 이벤트를 디스패치하는 컨트롤러를 테스트할 때, 이벤트 리스너까지 실제로 실행되게 하고 싶지 않을 수 있습니다. 리스너는 리스너대로 별도의 테스트 케이스에서 검증하면 되므로, 컨트롤러 테스트에서는 HTTP 응답만 확인하면 충분한 경우가 많기 때문입니다.
Laravel은 이벤트, Job, 그 밖의 여러 파사드를 손쉽게 모킹할 수 있는 도우미 메서드를 기본으로 제공합니다. 이 도우미들은 사실 Mockery 위에 편의 레이어를 얹은 것이라, 복잡한 Mockery API를 직접 다룰 필요 없이 간단하게 모킹을 처리할 수 있습니다.
객체 모킹
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');파사드 모킹
일반적인 정적(static) 메서드 호출과 달리, 파사드(그리고 실시간 파사드 포함)는 모킹이 가능합니다. 이는 전통적인 정적 메서드에 비해 큰 장점으로, 의존성 주입(dependency injection)을 사용할 때와 동일한 수준의 테스트 용이성을 제공합니다. 테스트를 작성하다 보면 컨트롤러 안에서 호출하는 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 파사드는 모킹하지 마세요. 대신 테스트를 실행할 때 HTTP 테스트 메서드인 get, post 등에 원하는 입력값을 직접 전달하세요. 마찬가지로 Config 파사드를 모킹하는 대신, 테스트 코드에서 Config::set 메서드를 직접 호출하는 것이 좋습니다.
파사드 스파이
파사드를 스파이로 사용하고 싶다면, 해당 파사드의 spy 메서드를 호출하면 됩니다. 스파이는 모킹과 비슷하지만, 스파이와 테스트 대상 코드 사이의 상호작용을 모두 기록해두었다가 코드 실행 후에 검증(assertion)할 수 있게 해줍니다:
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() 같은 헬퍼가 반환하는 시간을 원하는 값으로 바꿔야 할 때가 있습니다. 예를 들어 "가입 후 30일이 지난 사용자"를 테스트하려면 실제로 30일을 기다릴 수는 없겠죠. Laravel의 기본 기능 테스트(feature test) 클래스는 현재 시간을 자유롭게 조작할 수 있는 도우미 메서드를 제공합니다:
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()->mins(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());
}NOTE
시간 조작 헬퍼는 now(), Carbon::now()처럼 Laravel이 제어할 수 있는 시간 소스에만 적용됩니다. 데이터베이스의 NOW() 함수나 외부 API가 반환하는 타임스탬프처럼 애플리케이션 바깥에서 생성되는 시간 값에는 영향을 주지 않는다는 점에 유의하세요.