758 lines
16 KiB
Markdown
758 lines
16 KiB
Markdown
# Mocking
|
||
|
||
* Introduction
|
||
* Mocking Objects
|
||
* Mocking Facades
|
||
* Facade Spies
|
||
* Interacting With Time
|
||
|
||
## Introduction
|
||
|
||
When testing Laravel applications, you may wish to "mock" certain aspects of
|
||
your application so they are not actually executed during a given test. For
|
||
example, when testing a controller that dispatches an event, you may wish to
|
||
mock the event listeners so they are not actually executed during the test.
|
||
This allows you to only test the controller's HTTP response without worrying
|
||
about the execution of the event listeners since the event listeners can be
|
||
tested in their own test case.
|
||
|
||
Laravel provides helpful methods for mocking events, jobs, and other facades
|
||
out of the box. These helpers primarily provide a convenience layer over
|
||
Mockery so you do not have to manually make complicated Mockery method calls.
|
||
|
||
## Mocking Objects
|
||
|
||
When mocking an object that is going to be injected into your application via
|
||
Laravel's [service container](/docs/12.x/container), you will need to bind
|
||
your mocked instance into the container as an `instance` binding. This will
|
||
instruct the container to use your mocked instance of the object instead of
|
||
constructing the object itself:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1use App\Service;
|
||
|
||
2use Mockery;
|
||
|
||
3use Mockery\MockInterface;
|
||
|
||
4
|
||
|
||
5test('something can be mocked', function () {
|
||
|
||
6 $this->instance(
|
||
|
||
7 Service::class,
|
||
|
||
8 Mockery::mock(Service::class, function (MockInterface $mock) {
|
||
|
||
9 $mock->expects('process');
|
||
|
||
10 })
|
||
|
||
11 );
|
||
|
||
12});
|
||
|
||
|
||
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');
|
||
})
|
||
);
|
||
});
|
||
|
||
|
||
1use App\Service;
|
||
|
||
2use Mockery;
|
||
|
||
3use Mockery\MockInterface;
|
||
|
||
4
|
||
|
||
5public function test_something_can_be_mocked(): void
|
||
|
||
6{
|
||
|
||
7 $this->instance(
|
||
|
||
8 Service::class,
|
||
|
||
9 Mockery::mock(Service::class, function (MockInterface $mock) {
|
||
|
||
10 $mock->expects('process');
|
||
|
||
11 })
|
||
|
||
12 );
|
||
|
||
13}
|
||
|
||
|
||
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');
|
||
})
|
||
);
|
||
}
|
||
|
||
In order to make this more convenient, you may use the `mock` method that is
|
||
provided by Laravel's base test case class. For example, the following example
|
||
is equivalent to the example above:
|
||
|
||
|
||
|
||
1use App\Service;
|
||
|
||
2use Mockery\MockInterface;
|
||
|
||
3
|
||
|
||
4$mock = $this->mock(Service::class, function (MockInterface $mock) {
|
||
|
||
5 $mock->expects('process');
|
||
|
||
6});
|
||
|
||
|
||
use App\Service;
|
||
use Mockery\MockInterface;
|
||
|
||
$mock = $this->mock(Service::class, function (MockInterface $mock) {
|
||
$mock->expects('process');
|
||
});
|
||
|
||
You may use the `partialMock` method when you only need to mock a few methods
|
||
of an object. The methods that are not mocked will be executed normally when
|
||
called:
|
||
|
||
|
||
|
||
1use App\Service;
|
||
|
||
2use Mockery\MockInterface;
|
||
|
||
3
|
||
|
||
4$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
|
||
|
||
5 $mock->expects('process');
|
||
|
||
6});
|
||
|
||
|
||
use App\Service;
|
||
use Mockery\MockInterface;
|
||
|
||
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
|
||
$mock->expects('process');
|
||
});
|
||
|
||
Similarly, if you want to
|
||
[spy](http://docs.mockery.io/en/latest/reference/spies.html) on an object,
|
||
Laravel's base test case class offers a `spy` method as a convenient wrapper
|
||
around the `Mockery::spy` method. Spies are similar to mocks; however, spies
|
||
record any interaction between the spy and the code being tested, allowing you
|
||
to make assertions after the code is executed:
|
||
|
||
|
||
|
||
1use App\Service;
|
||
|
||
2
|
||
|
||
3$spy = $this->spy(Service::class);
|
||
|
||
4
|
||
|
||
5// ...
|
||
|
||
6
|
||
|
||
7$spy->shouldHaveReceived('process');
|
||
|
||
|
||
use App\Service;
|
||
|
||
$spy = $this->spy(Service::class);
|
||
|
||
// ...
|
||
|
||
$spy->shouldHaveReceived('process');
|
||
|
||
## Mocking Facades
|
||
|
||
Unlike traditional static method calls, [facades](/docs/12.x/facades)
|
||
(including [real-time facades](/docs/12.x/facades#real-time-facades)) may be
|
||
mocked. This provides a great advantage over traditional static methods and
|
||
grants you the same testability that you would have if you were using
|
||
traditional dependency injection. When testing, you may often want to mock a
|
||
call to a Laravel facade that occurs in one of your controllers. For example,
|
||
consider the following controller action:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use Illuminate\Support\Facades\Cache;
|
||
|
||
6
|
||
|
||
7class UserController extends Controller
|
||
|
||
8{
|
||
|
||
9 /**
|
||
|
||
10 * Retrieve a list of all users of the application.
|
||
|
||
11 */
|
||
|
||
12 public function index(): array
|
||
|
||
13 {
|
||
|
||
14 $value = Cache::get('key');
|
||
|
||
15
|
||
|
||
16 return [
|
||
|
||
17 // ...
|
||
|
||
18 ];
|
||
|
||
19 }
|
||
|
||
20}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
class UserController extends Controller
|
||
{
|
||
/**
|
||
* Retrieve a list of all users of the application.
|
||
*/
|
||
public function index(): array
|
||
{
|
||
$value = Cache::get('key');
|
||
|
||
return [
|
||
// ...
|
||
];
|
||
}
|
||
}
|
||
|
||
We can mock the call to the `Cache` facade by using the `expects` method,
|
||
which will return an instance of a
|
||
[Mockery](https://github.com/padraic/mockery) mock. Since facades are actually
|
||
resolved and managed by the Laravel [service container](/docs/12.x/container),
|
||
they have much more testability than a typical static class. For example,
|
||
let's mock our call to the `Cache` facade's `get` method:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use Illuminate\Support\Facades\Cache;
|
||
|
||
4
|
||
|
||
5test('get index', function () {
|
||
|
||
6 Cache::expects('get')
|
||
|
||
7 ->with('key')
|
||
|
||
8 ->andReturn('value');
|
||
|
||
9
|
||
|
||
10 $response = $this->get('/users');
|
||
|
||
11
|
||
|
||
12 // ...
|
||
|
||
13});
|
||
|
||
|
||
<?php
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
test('get index', function () {
|
||
Cache::expects('get')
|
||
->with('key')
|
||
->andReturn('value');
|
||
|
||
$response = $this->get('/users');
|
||
|
||
// ...
|
||
});
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace Tests\Feature;
|
||
|
||
4
|
||
|
||
5use Illuminate\Support\Facades\Cache;
|
||
|
||
6use Tests\TestCase;
|
||
|
||
7
|
||
|
||
8class UserControllerTest extends TestCase
|
||
|
||
9{
|
||
|
||
10 public function test_get_index(): void
|
||
|
||
11 {
|
||
|
||
12 Cache::expects('get')
|
||
|
||
13 ->with('key')
|
||
|
||
14 ->andReturn('value');
|
||
|
||
15
|
||
|
||
16 $response = $this->get('/users');
|
||
|
||
17
|
||
|
||
18 // ...
|
||
|
||
19 }
|
||
|
||
20}
|
||
|
||
|
||
<?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');
|
||
|
||
// ...
|
||
}
|
||
}
|
||
|
||
You should not mock the `Request` facade. Instead, pass the input you desire
|
||
into the [HTTP testing methods](/docs/12.x/http-tests) such as `get` and
|
||
`post` when running your test. Likewise, instead of mocking the `Config`
|
||
facade, call the `Config::set` method in your tests.
|
||
|
||
### Facade Spies
|
||
|
||
If you would like to
|
||
[spy](http://docs.mockery.io/en/latest/reference/spies.html) on a facade, you
|
||
may call the `spy` method on the corresponding facade. Spies are similar to
|
||
mocks; however, spies record any interaction between the spy and the code
|
||
being tested, allowing you to make assertions after the code is executed:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use Illuminate\Support\Facades\Cache;
|
||
|
||
4
|
||
|
||
5test('values are stored in cache', function () {
|
||
|
||
6 Cache::spy();
|
||
|
||
7
|
||
|
||
8 $response = $this->get('/');
|
||
|
||
9
|
||
|
||
10 $response->assertStatus(200);
|
||
|
||
11
|
||
|
||
12 Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
|
||
|
||
13});
|
||
|
||
|
||
<?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);
|
||
});
|
||
|
||
|
||
1use Illuminate\Support\Facades\Cache;
|
||
|
||
2
|
||
|
||
3public function test_values_are_stored_in_cache(): void
|
||
|
||
4{
|
||
|
||
5 Cache::spy();
|
||
|
||
6
|
||
|
||
7 $response = $this->get('/');
|
||
|
||
8
|
||
|
||
9 $response->assertStatus(200);
|
||
|
||
10
|
||
|
||
11 Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
|
||
|
||
12}
|
||
|
||
|
||
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);
|
||
}
|
||
|
||
## Interacting With Time
|
||
|
||
When testing, you may occasionally need to modify the time returned by helpers
|
||
such as `now` or `Illuminate\Support\Carbon::now()`. Thankfully, Laravel's
|
||
base feature test class includes helpers that allow you to manipulate the
|
||
current time:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1test('time can be manipulated', function () {
|
||
|
||
2 // Travel into the future...
|
||
|
||
3 $this->travel(5)->milliseconds();
|
||
|
||
4 $this->travel(5)->seconds();
|
||
|
||
5 $this->travel(5)->minutes();
|
||
|
||
6 $this->travel(5)->hours();
|
||
|
||
7 $this->travel(5)->days();
|
||
|
||
8 $this->travel(5)->weeks();
|
||
|
||
9 $this->travel(5)->years();
|
||
|
||
10
|
||
|
||
11 // Travel into the past...
|
||
|
||
12 $this->travel(-5)->hours();
|
||
|
||
13
|
||
|
||
14 // Travel to an explicit time...
|
||
|
||
15 $this->travelTo(now()->subHours(6));
|
||
|
||
16
|
||
|
||
17 // Return back to the present time...
|
||
|
||
18 $this->travelBack();
|
||
|
||
19});
|
||
|
||
|
||
test('time can be manipulated', function () {
|
||
// Travel into the future...
|
||
$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();
|
||
|
||
// Travel into the past...
|
||
$this->travel(-5)->hours();
|
||
|
||
// Travel to an explicit time...
|
||
$this->travelTo(now()->subHours(6));
|
||
|
||
// Return back to the present time...
|
||
$this->travelBack();
|
||
});
|
||
|
||
|
||
1public function test_time_can_be_manipulated(): void
|
||
|
||
2{
|
||
|
||
3 // Travel into the future...
|
||
|
||
4 $this->travel(5)->milliseconds();
|
||
|
||
5 $this->travel(5)->seconds();
|
||
|
||
6 $this->travel(5)->minutes();
|
||
|
||
7 $this->travel(5)->hours();
|
||
|
||
8 $this->travel(5)->days();
|
||
|
||
9 $this->travel(5)->weeks();
|
||
|
||
10 $this->travel(5)->years();
|
||
|
||
11
|
||
|
||
12 // Travel into the past...
|
||
|
||
13 $this->travel(-5)->hours();
|
||
|
||
14
|
||
|
||
15 // Travel to an explicit time...
|
||
|
||
16 $this->travelTo(now()->subHours(6));
|
||
|
||
17
|
||
|
||
18 // Return back to the present time...
|
||
|
||
19 $this->travelBack();
|
||
|
||
20}
|
||
|
||
|
||
public function test_time_can_be_manipulated(): void
|
||
{
|
||
// Travel into the future...
|
||
$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();
|
||
|
||
// Travel into the past...
|
||
$this->travel(-5)->hours();
|
||
|
||
// Travel to an explicit time...
|
||
$this->travelTo(now()->subHours(6));
|
||
|
||
// Return back to the present time...
|
||
$this->travelBack();
|
||
}
|
||
|
||
You may also provide a closure to the various time travel methods. The closure
|
||
will be invoked with time frozen at the specified time. Once the closure has
|
||
executed, time will resume as normal:
|
||
|
||
|
||
|
||
1$this->travel(5)->days(function () {
|
||
|
||
2 // Test something five days into the future...
|
||
|
||
3});
|
||
|
||
4
|
||
|
||
5$this->travelTo(now()->subDays(10), function () {
|
||
|
||
6 // Test something during a given moment...
|
||
|
||
7});
|
||
|
||
|
||
$this->travel(5)->days(function () {
|
||
// Test something five days into the future...
|
||
});
|
||
|
||
$this->travelTo(now()->subDays(10), function () {
|
||
// Test something during a given moment...
|
||
});
|
||
|
||
The `freezeTime` method may be used to freeze the current time. Similarly, the
|
||
`freezeSecond` method will freeze the current time but at the start of the
|
||
current second:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Carbon;
|
||
|
||
2
|
||
|
||
3// Freeze time and resume normal time after executing closure...
|
||
|
||
4$this->freezeTime(function (Carbon $time) {
|
||
|
||
5 // ...
|
||
|
||
6});
|
||
|
||
7
|
||
|
||
8// Freeze time at the current second and resume normal time after executing closure...
|
||
|
||
9$this->freezeSecond(function (Carbon $time) {
|
||
|
||
10 // ...
|
||
|
||
11})
|
||
|
||
|
||
use Illuminate\Support\Carbon;
|
||
|
||
// Freeze time and resume normal time after executing closure...
|
||
$this->freezeTime(function (Carbon $time) {
|
||
// ...
|
||
});
|
||
|
||
// Freeze time at the current second and resume normal time after executing closure...
|
||
$this->freezeSecond(function (Carbon $time) {
|
||
// ...
|
||
})
|
||
|
||
As you would expect, all of the methods discussed above are primarily useful
|
||
for testing time sensitive application behavior, such as locking inactive
|
||
posts on a discussion forum:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1use App\Models\Thread;
|
||
|
||
2
|
||
|
||
3test('forum threads lock after one week of inactivity', function () {
|
||
|
||
4 $thread = Thread::factory()->create();
|
||
|
||
5
|
||
|
||
6 $this->travel(1)->week();
|
||
|
||
7
|
||
|
||
8 expect($thread->isLockedByInactivity())->toBeTrue();
|
||
|
||
9});
|
||
|
||
|
||
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();
|
||
});
|
||
|
||
|
||
1use App\Models\Thread;
|
||
|
||
2
|
||
|
||
3public function test_forum_threads_lock_after_one_week_of_inactivity()
|
||
|
||
4{
|
||
|
||
5 $thread = Thread::factory()->create();
|
||
|
||
6
|
||
|
||
7 $this->travel(1)->week();
|
||
|
||
8
|
||
|
||
9 $this->assertTrue($thread->isLockedByInactivity());
|
||
|
||
10}
|
||
|
||
|
||
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());
|
||
}
|
||
|