Mocking trong Laravel

Giới thiệu

Khi Test ứng dụng Laravel, bạn có thể muốn “mock” một vài khía cạnh nhất định của ứng dụng để chúng không được thực hiện trong khi test. Ví dụ, khi test một controller mà gắn với một sự kiện, bạn có thể muốn dừng việc lắng nghe sự kiện để chúng không được thực hiện trong khi test. Điều đó cho phép bạn chỉ test response HTTP của controller mà không lo đến việc thưc thi của các event listener, từ đó các event listener có thể được kiểm tra trong các test case riêng.

Laravel cung cấp các công cụ cho các event mocking, jobs, và các facade bên ngoài. Các công cụ đó chủ yếu cung cấp một layer tiện lợi trên Mockery để bạn không phải tạo thủ công các việc goi hàm Mockery một cách phức tạp.

Các Mocking object

Khi đang mock một đối tượng mà nó đang được thêm vào ứng dụng của bạn thông qua các service container của Laravel, banj sẽ cần liên kết các đối tượng được mock vào trong container như là một instance đang liên kết. Điều này sẽ hướng dẫn container sử dụng đối tượng được mock thay cho đối tượng tự xây dựng:

use App\Service;
use Mockery;

$this->instance(Service::class, Mockery::mock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
}));

Để cho tiện hơn, bạn có thể sử dụng phương thức mock, cái mà được cung cấp bởi base test case của Laravel:

use App\Service;

$this->mock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
});

Bạn có thể sử dụng phương thức partialMock khi bạn chỉ cần môt vài phương thức của đối tượng. Các phương thức mà không được mock sẽ được thực thi bình thường khi được gọi:

use App\Service;

$this->partialMock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
});

Tương tự, nếu bạn cần spy (giả lập) môt đối tượng, class test case cơ bản của Laravel đề xuất một phương thức spy bao ngoài phương thức Mockery::spy

use App\Service;

$this->spy(Service::class, function ($mock) {
    $mock->shouldHaveReceived('process');
});

Bus fake

Như một thay thế cho việc mock, bạn có thể sử dụng Bus phương thức fake của facede để ngăn các việc đang được thực thi. Khi sử dụng các fake, các xác nhận được tạo ra sau khi code bên dưới test được thực thi:

<?php

namespace Tests\Feature;

use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Bus::fake();

        // Perform order shipping...

        Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) {
            return $job->order->id === $order->id;
        });

        // Assert a job was not dispatched...
        Bus::assertNotDispatched(AnotherJob::class);
    }
}

Event fake

Như là một thay thế cho việc mock, bạn có thể sử dụng Event phương thức fake của facade để dừng toàn bộ các event listener đang được thực thi. Bạn có thể xác nhận rằng các event đã được gửi đi và cả dữ liệu chúng nhận được. Khi sử dụng các fake, viêc xác nhận tạo ra sau khi code bên dưới được thực thi:

<?php

namespace Tests\Feature;

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function testOrderShipping()
    {
        Event::fake();

        // Perform order shipping...

        Event::assertDispatched(OrderShipped::class, function ($e) use ($order) {
            return $e->order->id === $order->id;
        });

        // Assert an event was dispatched twice...
        Event::assertDispatched(OrderShipped::class, 2);

        // Assert an event was not dispatched...
        Event::assertNotDispatched(OrderFailedToShip::class);
    }
}

Sau khi gọi Event::fake(), không có các event listener sẽ được thực thi. Vậy, nếu test của bạn sử dụng các model factory mà dựa trên các event, giống như là đang tạo một UDDI trong khi một event creating của model, bạn nên gọi Event::fake() sau khi sử dụng các factory của bạn.

Giả lập một tâp các sự kiện

Nếu bạn chỉ muốn giả lập các event listener cho một tập các event, bạn có thể truyền chúng đến phương thức fake hoặc fakeFor:

/**
 * Test order process.
 */
public function testOrderProcess()
{
    Event::fake([
        OrderCreated::class,
    ]);

    $order = factory(Order::class)->create();

    Event::assertDispatched(OrderCreated::class);

    // Other events are dispatched as normal...
    $order->update([...]);
}

#Giới hạn event fakes
Nếu bạn chỉ muốn giả lập các event listener cho một phần test của bạn, ban có thể sử dụng phương thức fakerFor:

<?php

namespace Tests\Feature;

use App\Events\OrderCreated;
use App\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * Test order process.
     */
    public function testOrderProcess()
    {
        $order = Event::fakeFor(function () {
            $order = factory(Order::class)->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // Events are dispatched as normal and observers will run ...
        $order->update([...]);
    }
}

#Giả lập mail
Bạn có thể sử dụng Mail phương thức fake của facade để dừng mail được gửi đi. Bạn có thể xác nhận rằng mailables đã được gửi đến người dùng và cả dữ liệu họ nhận được. Khi sử dụng các fake, việc xác nhận tạo ra sau khi code bên dưới test được thực thi:

<?php

namespace Tests\Feature;

use App\Mail\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Mail::fake();

        // Assert that no mailables were sent...
        Mail::assertNothingSent();

        // Perform order shipping...

        Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
            return $mail->order->id === $order->id;
        });

        // Assert a message was sent to the given users...
        Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
            return $mail->hasTo($user->email) &&
                   $mail->hasCc('...') &&
                   $mail->hasBcc('...');
        });

        // Assert a mailable was sent twice...
        Mail::assertSent(OrderShipped::class, 2);

        // Assert a mailable was not sent...
        Mail::assertNotSent(AnotherMailable::class);
    }
}

Nếu bạn đang dùng queue mailables để gửi bên trong, bạn nên dùng phương thức assertQueued thay cho assertSent:

Mail::assertQueued(...);
Mail::assertNotQueued(...);

#Giả lập thông báo

Bạn có thể sử dụng Notification phương thức fake của facade để dừng các thông báo được gửi đi. Bạn có thể xác nhận notifications được gửi đến người dùng và cả dữ liệu họ nhận được. Khi dùng các fake, việc xác nhận được tạo ra sau khi code bên dưới test được thực thi

<?php

namespace Tests\Feature;

use App\Notifications\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Notification::fake();

        // Assert that no notifications were sent...
        Notification::assertNothingSent();

        // Perform order shipping...

        Notification::assertSentTo(
            $user,
            OrderShipped::class,
            function ($notification, $channels) use ($order) {
                return $notification->order->id === $order->id;
            }
        );

        // Assert a notification was sent to the given users...
        Notification::assertSentTo(
            [$user], OrderShipped::class
        );

        // Assert a notification was not sent...
        Notification::assertNotSentTo(
            [$user], AnotherNotification::class
        );

        // Assert a notification was sent via Notification::route() method...
        Notification::assertSentTo(
            new AnonymousNotifiable, OrderShipped::class
        );

        // Assert Notification::route() method sent notification to the correct user...
        Notification::assertSentTo(
            new AnonymousNotifiable,
            OrderShipped::class,
            function ($notification, $channels, $notifiable) use ($user) {
                return $notifiable->routes['mail'] === $user->email;
            }
        );
    }
}

Giả lập Queue

Như là một thay thế cho việc mock, bạn có thể dùng Queue phương thức fake của facade để dừng các việc đang trong queue. Bạn có thể xác nhận rằng các việc đã được đưa vào queue và cả dữ liệu họ nhận được. Khi sử dụng các fake, việc xác nhận được tạo ra sau khi code bên dưới test được thực thi:

<?php

namespace Tests\Feature;

use App\Jobs\AnotherJob;
use App\Jobs\FinalJob;
use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Queue::fake();

        // Assert that no jobs were pushed...
        Queue::assertNothingPushed();

        // Perform order shipping...

        Queue::assertPushed(ShipOrder::class, function ($job) use ($order) {
            return $job->order->id === $order->id;
        });

        // Assert a job was pushed to a given queue...
        Queue::assertPushedOn('queue-name', ShipOrder::class);

        // Assert a job was pushed twice...
        Queue::assertPushed(ShipOrder::class, 2);

        // Assert a job was not pushed...
        Queue::assertNotPushed(AnotherJob::class);

        // Assert a job was pushed with a given chain of jobs, matching by class...
        Queue::assertPushedWithChain(ShipOrder::class, [
            AnotherJob::class,
            FinalJob::class
        ]);

        // Assert a job was pushed with a given chain of jobs, matching by both class and properties...
        Queue::assertPushedWithChain(ShipOrder::class, [
            new AnotherJob('foo'),
            new FinalJob('bar'),
        ]);

        // Assert a job was pushed without a chain of jobs...
        Queue::assertPushedWithoutChain(ShipOrder::class);
    }
}

#Giả lập lưu trữ

Storage phương thức fake của facade cho phép bạn dễ dàng tạo ra một ổ đĩa giả lập, kết hợp với việc tạo ra file tiện lợi từ class UploadedFile, đơn giản hơn nhiều cho việc kiểm tra việc upload file:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testAlbumUpload()
    {
        Storage::fake('photos');

        $response = $this->json('POST', '/photos', [
            UploadedFile::fake()->image('photo1.jpg'),
            UploadedFile::fake()->image('photo2.jpg')
        ]);

        // Assert one or more files were stored...
        Storage::disk('photos')->assertExists('photo1.jpg');
        Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);

        // Assert one or more files were not stored...
        Storage::disk('photos')->assertMissing('missing.jpg');
        Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
    }
}

Mạc định, phương thức fake sẽ xóa toàn bộ các file trong thư mục tạm thời. Nếu bạn muốn giữ lại các file đó , bạn có thể dùng phương thức “persistentFake” để thay thế.

Facade

Không giống như việc gọi hàm thông thường, facade có thể được mock. Điều này cung cấp một cải tiến tuyệt vời so với các hàm static truyền thống và cung cấp cho bạn khả năng kiểm tra tương tự khi bạn muốn muốn thêm vào các dependency. Khi test, bạn có thể thường xuyên muốn mock một lời gọi đến Laravel facade trong một controller của bạn. Ví dụ xem xét hành động của controller sau:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * Show a list of all users of the application.
     *
     * @return Response
     */
    public function index()
    {
        $value = Cache::get('key');

        //
    }
}

Bạn có thể mock lời goi hàm đến Cache facade bằng cách dùng phương thức shouldReceive, cái mà sẽ trả về môt đối tượng của một Mockery mock. Vì các facade thực sự được giải quyết và quản lý bởi Laravel service container , chúng có nhiều khả nằng kiểm tra hơn một static class thông thường. Ví dụ, hãy mock lời gọi hàm của chúng ra đến phương thức Cache của facade:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $response = $this->get('/users');

        // ...
    }
}

Bạn không nên mock Request facade. Thay vào đó, truyền input bạn muốn vào bên trong các phương thức HTTP như là getpost khi đang chạy test. Tương tự, thay cho việc mock Config facde, gọi phương thức Config::set trong các test của bạn.