Cơ bản về Mocking trong Laravel - Mockery

Cơ bản về Mocking trong Laravel - Mockery

Trong quá trình viết unit test cho ứng dụng Laravel, chắc hẳn các bạn đã từng dùng đến các Mock Objects. Tuy nhiên, với những người mới tiếp xúc với Mocking thì có thể chưa hiểu rõ tại sao mình phải thực hiện mocking mà chỉ viết theo "code cũ để lại" =)). Vì vậy trong bài viết này mình sẽ cùng đi tìm hiểu về những thứ cơ bản nhất về Mocking và một trong những mock object framework nổi tiếng nhất dành cho PHP đó là Mockery nhé.

1. Mocking là gì

Mocking là một cách tiếp cận với unit test, sử dụng các mock object để thực hiện “làm giả” các phương thức tương tác giữa các đối tượng trong module khác. Trong mock testing, các đối tượng phụ thuộc bên ngoài sẽ được thay thế bằng một bản mô phỏng với các hành vi giống như đối tượng thật.
Ví dụ nếu bạn viết test cho một controller, khi dispatch một event thì bạn có thể chỉ cần "mock" kết quả của event này để chúng không thực sự được thực thi trong hàm quá trình test. Và nó cho phép bạn kiểm tra kết quả mà không cần lo lắng về hàm xử lý event vì có thể phần này sẽ được test trong những test case riêng.

2. Lợi ích khi dùng Mocking

Tính độc lập
Trong điều kiện lý tưởng, mỗi hàm test trong Unit Test chỉ phụ trách một chức năng nhất định và không phụ thuộc lẫn nhau. Sử dụng Mocking sẽ cho phép ta mô phỏng các chức năng không thuộc pham vi test, đảm bảo được sự chính xác cho hàm test của mình. Sẽ không có trường hợp test failed do một chức năng liên quan (nằm ngoài phạm vi test) bị lỗi.
Mô phỏng những tình huống khó tái hiện
Với những tình huống như gửi mail cho user, gọi đến service của bên thứ 3 (Facebook, Google, ...) sẽ có rất nhiều vấn đề xảy ra nếu như chúng ta không dùng mocking. Việc kiểm tra xem hành động gửi mail đã được thực hiện chưa, hay trường hợp service bị lỗi khiến ta không tái hiện được, là những trường hợp khó giải quyết. Với mocking thì mọi chuyện dễ dàng hơn rất nhiều khi chúng ta có thể tùy ý chọn bất cứ kết quả trả về nào mà mình mong muốn.
Tốc độ
Việc sử dụng MockObject mô phỏng lại kết quả của các chức năng nằm ngoài phạm vi test thay vì thực hiện các chức năng đó, tốc độ xử lý hàm test sẽ được cải thiện rõ rệt. Điều này đặc biệt quan trọng khi làm việc với các dự án lớn, với rất nhiều hàm test cần phải chạy.

3. Mockery

  1. Giới thiệu và cách cài đặt
    Mockery là một framework nổi tiếng dùng để hỗ trợ việc mock object trong PHP. Từ phiên bản Laravel 5.4 trở đi, Mockery được cài đặt cùng với ứng dụng. Còn đối với các phiên bản cũ hơn thì ta có thể cài đặt Mockery thông qua composer:
    composer require --dev mockery/mockery

  2. Cơ bản về cách sử dụng Mockery

Khởi tạo Mock Object

$userMock = Mockery::mock(User::class);

// Truyền tham số khi khởi tạo
$userMock = Mockery::mock(User::class, [$username, $address]);

shouldReceive() & andReturn()
Khai báo shouldReceive($methodName) sẽ chỉ định những method nào mà Mock Object sẽ gọi. Và andReturn($result) sẽ chỉ định kết quả trả về.
Ví dụ:

$userMock = Mockery::mock(User::class);

// $userMock gọi tới hàm getFullName và nhận kết quả trả về là 'Haposoft'
// mà không cần thực thi hàm getFullName
$userMock->shouldReceive('getFullName')->andReturn('Haposoft');

Ta có thể khai báo nhiều method trong shouldReceive:

$userMock->shouldReceive(['getFullName', 'getAddress']);

// Khai báo các giá trị trả về tương ứng với từng method:
$userMock->shouldReceive([
    'getFullName' => 'Haposoft',
    'getAddress' => 'Ha Noi'
]);

// Hoặc có thể khai báo một cách ngắn gọn như sau:
$userMock = Mockery::mock(User::class, ['getFullName' => 'Haposoft', 'getAddress' => 'Ha Noi']);

Tham số đầu vào
Mặc định, Mockery sẽ khởi tạo các hàm với option withAnyArgs(). Tuy nhiên nếu ta muốn việc test case chặt chẽ hơn thì có thể xác định đầu vào bằng cách sử dụng with():

$userMock->shouldReceive('getFullName')->with('id', 'username');
// hoặc
$userMock->shouldReceive('getFullName')->withArgs(['id', 'username'])

// Khai báo rằng method được gọi không có tham số truyền vào
$userMock->shouldReceive('getFullName')->withNoArgs();

Cần lưu ý rằng, nếu bạn khai báo tham số trong with() không giống với tham số khi gọi thực tế thì có thể xảy ra lỗi:

$userMock->shouldReceive('getFullName')->with('id');

$userMock->getFullName('username'); // throws a NoMatchingExpectationException

makePartial()
Option này giúp cho những chức năng mà ta không khai báo thì vẫn sẽ được gọi như bình thường. Có hai cách làm, hoặc bạn có thể khai báo bằng cách truyền vào tên những chức năng bạn sẽ mock:

$userMock = Mockery::mock('App\User[getFullName]');

hoặc khai báo makePartial cho Mock Object sau khi khởi tạo:

$userMock = Mockery::mock(User::class)->makePartial();

Với cách khai báo thứ 2, chỉ những phương thức mà bạn khai báo trong shouldReceive() thì mới được thực hiện mock, còn lại thì sẽ thực hiện một cách bình thường.

shouldAllowMockingProtectedMethods()
Mặc định thì Mockery không cho phép bạn mock các protected method. Tuy nhiên trong một số trường hợp bắt buộc thì bạn có thể sử dụng option shouldAllowMockingProtectedMethods() cho phép mock protected method cho một class cụ thể:

class User
{
    protected function setPassword()
    {
    }
}

$userMock = Mockery::mock(User::class)->shouldAllowMockingProtectedMethods();
$userMock->shouldReceive('setPassword');

4. Kết luận

Trong bài viết này mình và các bạn đã cùng đi tìm hiểu những khái niệm cơ bản nhất về Mocking trong Laravel và cách sử dụng Mockery. Hi vọng các bạn có thể hiểu và áp dụng trong các dự án thực tế.

Cảm ơn các bạn đã đọc bài viết! Xin chào và hẹn gặp lại!

Tài liệu tham khảo:
http://docs.mockery.io/en/latest/index.html
https://laravel.com/docs/8.x/mocking
https://viblo.asia/p/mocking-trong-laravel-bWrZnWbQlxw