모의(Mocking)
소개
Laravel 애플리케이션을 테스트할 때 특정 테스트 중에 실제로 실행되지 않도록 애플리케이션의 특정 부분을 "모의"처리하고 싶을 수 있습니다. 예를 들어, 이벤트를 디스패치하는 컨트롤러를 테스트할 때 이벤트 리스너가 테스트 중에 실제로 실행되지 않도록 모의 처리하고 싶을 수 있습니다. 이렇게 하면 이벤트 리스너는 자체 테스트 케이스에서 테스트할 수 있으므로 이벤트 리스너 실행에 대해 걱정하지 않고 컨트롤러의 HTTP 응답만 테스트할 수 있습니다.
Laravel은 이벤트, 작업 및 기타 퍼사드를 즉시 모의 처리하는 데 유용한 메서드를 제공합니다. 이러한 도우미는 주로 Mockery에 대한 편의 계층을 제공하므로 복잡한 Mockery 메서드 호출을 수동으로 만들 필요가 없습니다.
객체 모의 처리
Laravel의 서비스 컨테이너를 통해 애플리케이션에 주입될 객체를 모의 처리할 때 모의 처리된 인스턴스를 컨테이너에 instance 바인딩으로 바인딩해야 합니다. 이렇게 하면 컨테이너가 객체 자체를 생성하는 대신 객체의 모의 처리된 인스턴스를 사용하도록 지시합니다.
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->shouldReceive('process')->once(); }) );});
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) { // 'process' 메서드가 한 번 호출될 것으로 예상되는 모의 객체를 설정합니다. $mock->shouldReceive('process')->once(); }) );}
더욱 편리하게 사용하기 위해 Laravel의 기본 테스트 케이스 클래스에서 제공하는 mock 메서드를 사용할 수 있습니다. 예를 들어, 다음 예시는 위의 예시와 동일합니다.
use App\Service;use Mockery\MockInterface; $mock = $this->mock(Service::class, function (MockInterface $mock) { $mock->shouldReceive('process')->once();});
객체의 일부 메서드만 모의해야 하는 경우에는 partialMock 메서드를 사용할 수 있습니다. 모의되지 않은 메서드는 호출될 때 정상적으로 실행됩니다.
use App\Service;use Mockery\MockInterface; $mock = $this->partialMock(Service::class, function (MockInterface $mock) { $mock->shouldReceive('process')->once();});
마찬가지로, 객체를 스파이하고 싶다면 Laravel의 기본 테스트 케이스 클래스는 Mockery::spy 메서드를 편리하게 감싼 spy 메서드를 제공합니다. 스파이는 모의와 유사합니다. 그러나 스파이는 스파이와 테스트 중인 코드 간의 모든 상호 작용을 기록하여 코드가 실행된 후 어설션을 수행할 수 있습니다.
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 [ // ... ]; }}
Mockery 모의 인스턴스를 반환하는 shouldReceive 메서드를 사용하여 Cache 퍼사드에 대한 호출을 모의할 수 있습니다. 퍼사드는 실제로 Laravel 서비스 컨테이너에 의해 해결되고 관리되기 때문에 일반적인 정적 클래스보다 훨씬 더 많은 테스트 가능성을 가집니다. 예를 들어, Cache 퍼사드의 get 메서드에 대한 호출을 모의해 보겠습니다.
<?php use Illuminate\Support\Facades\Cache; test('get index', function () { Cache::shouldReceive('get') ->once() ->with('key') ->andReturn('value'); $response = $this->get('/users'); // ...});
<?php namespace Tests\Feature; use Illuminate\Support\Facades\Cache;use Tests\TestCase; class UserControllerTest extends TestCase{ public function test_get_index(): void { Cache::shouldReceive('get') ->once() ->with('key') ->andReturn('value'); $response = $this->get('/users'); // ... }}
Request 파사드를 모의(mock)해서는 안 됩니다. 대신, 테스트를 실행할 때 get 및 post와 같은 HTTP 테스트 메소드에 원하는 입력을 전달하세요. 마찬가지로, Config 파사드를 모의하는 대신 테스트에서 Config::set 메소드를 호출하세요.
파사드 스파이
파사드를 스파이하고 싶다면, 해당 파사드에서 spy 메소드를 호출하면 됩니다. 스파이는 모의와 유사하지만, 스파이는 스파이와 테스트 중인 코드 간의 모든 상호 작용을 기록하여 코드가 실행된 후 어설션을 수행할 수 있습니다.
<?php use Illuminate\Support\Facades\Cache; test('values are be stored in cache', function () { Cache::spy(); $response = $this->get('/'); $response->assertStatus(200); Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);});
use Illuminate\Support\Facades\Cache; public function test_values_are_be_stored_in_cache(): void{ Cache::spy(); $response = $this->get('/'); $response->assertStatus(200); Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);}
시간 조작하기
테스트를 할 때, now 또는 Illuminate\Support\Carbon::now()와 같은 도우미 함수가 반환하는 시간을 수정해야 할 경우가 종종 있습니다. 다행히도 라라벨의 기본 기능 테스트 클래스에는 현재 시간을 조작할 수 있는 도우미 함수가 포함되어 있습니다.
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()->subHours(6)); // 현재 시간으로 되돌아가기... $this->travelBack();});
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()->subHours(6)); // 현재 시간으로 되돌아가기... $this->travelBack();}
다양한 시간 여행 메서드에 클로저를 제공할 수도 있습니다. 클로저는 지정된 시간에 시간이 멈춘 상태로 호출됩니다. 클로저가 실행되면 시간이 정상적으로 재개됩니다.
$this->travel(5)->days(function () { // 5일 후의 상황을 테스트합니다...}); $this->travelTo(now()->subDays(10), function () { // 특정 시점의 상황을 테스트합니다...});
freezeTime 메서드는 현재 시간을 고정하는 데 사용할 수 있습니다. 마찬가지로 freezeSecond 메서드는 현재 시간을 고정하지만 현재 초의 시작 부분으로 고정합니다.
use Illuminate\Support\Carbon; // 시간을 고정하고 클로저 실행 후 정상 시간으로 재개합니다...$this->freezeTime(function (Carbon $time) { // ...}); // 현재 초에 시간을 고정하고 클로저 실행 후 정상 시간으로 재개합니다...$this->freezeSecond(function (Carbon $time) { // ...})
예상대로 위에서 설명한 모든 메서드는 주로 토론 포럼에서 비활성 게시물을 잠그는 것과 같이 시간에 민감한 응용 프로그램 동작을 테스트하는 데 유용합니다.
use App\Models\Thread; test('포럼 스레드는 일주일 동안 비활성 상태이면 잠깁니다.', function () { $thread = Thread::factory()->create(); $this->travel(1)->week(); expect($thread->isLockedByInactivity())->toBeTrue();});
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());}