Testing with Mockery in Laravel

Today, we're kinda running low on time, so just skip all the fun part, and let's dig right into the "proper" part.

Khi viết unit test, thông thường, và cơ bản nhất, chúng ta thường chỉ viết test cho phần xử lí logic, ví dụ như Model hay Repository. Khi đặt ra yêu cầu cần viết unit test cho phần controller, nếu chỉ sử dụng phpunit, package mặc định đi kèm trong laravel, bạn có thể sẽ phải khá lúng túng. Mockery là một framework test trong PHP, sẽ là một công cụ khá hữu hiệu dành cho bạn trong trường hợp này.

Dùng Mock để làm gì ?

Trong điều kiện lí tưởng, khi bạn viết unit test, mỗi hàm test sẽ chỉ phụ trách một phần chức năng nhất định, và không phụ thuộc lẫn nhau. Điều đó có nghĩa là, nếu ta đã có code test cho phần logic, lấy dữ liệu từ database ra chẳng hạn, thì khi test trên controller, ta không cần phải quan tâm đến chuyện phía database có làm việc đúng hay không. Sử dụng Mockery tạo ra các Mock Object sẽ cho phép ta mô phỏng các chức năng không thuộc phạm vi trách nhiệm của mình, để đảm bảo tính độc lập của test. Các lợi ích mà việc làm này mang lại

Tính biệt lập

Với việc tạo ra các Mock Object để mô phỏng các chức năng không thuộc phạm vi test, ta có thể đảm bảo được sự chính xác cho phép test của mình. Sẽ không có chuyện test bị fail, do một chức năng liên quan ( nằm ngoài phạm vi phép test ) bị lỗi.

Test được những case mà kết quả khó xác định

Ví dụ, trong hàm ta cần test, có thực hiện những hành động như: gửi email cho user. Khi test, ta sẽ không thể kiểm tra xem hành động này đã được thực hiện đúng hay chưa. Bằng việc dùng Mock Object, ta có thể mô phỏng, coi như việc gửi email đã xảy ra ( mà không cần thay đổi source code ). Một lợi ích khác của việc dùng Mock Object ở đây là ta có thậm chí không cần phải thực sự thực hiện việc gửi mail.

Mô phỏng những tình huống khó có thể tái hiện

Ví dụ như, ta đang test xử lí cho trường hợp gọi đến service của bên thứ 3 mà service này đang bị lỗi. Rõ ràng, ta không thể kiểm soát được service của người khác, nên tái hiện thực tế case này là việc quá khó. Với Mock Object, ta có thể tùy ý mô phỏng, để việc request trả về bất cứ trạng thái / kết quả nào mình muốn.

Tốc độ

Bằng việc sử dụng Mock Object 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ật sự thực hiện các chức năng đó, tốc độ xử lí hàm test của ta sẽ được cải thiện rõ rệt. Điều này đặc biệt qua trọng khi ứng dụng của bạn lớn, với nhiều hàm test cần phải chạy. Hãy tưởng tượng, thay vì truy vấn đến database để lấy dữ liệu 1000 lần, ta tạo sẵn bô dữ liệu trả về. Rõ ràng ở đây, thời gian ta tiết kiệm được là khá đáng kể.

Tính độc lập

Tương tự như tính biệt lập, việc sử dụng Mock Object sẽ khiến cho ta có khả năng test được chức năng của mình, ngay cả khi chức năng mà mình phụ thuộc ( có thể do người khác thực hiện ) còn chưa có sẵn.

Sử dụng Mockery

Trước tiên là việc cài đặt. Là một trong những package thông dụng nhất của PHP, việc cài đặt Mockery thường được thực hiện khá dễ dang thông qua composer

{
  "require-dev": {
    "mockery/mockery": "[email protected]"
  }
}

Các hàm thường được sử dụng của Mockery gồm có

Khởi tạo Mock Object

Cách đơn giản nhất , ta có thể viết

$mock = Mockery::mock();

Nếu như ta cần tạo Mock Object dựa trên một class cụ thể, ta có thể truyền thêm tham số tên class

$mock = Mockery::mock('\Path\To\Class');

Hay trong trường hợp object mà ta muốn mock có thể khởi tạo với tham số, ta có thể truyền thêm vào như sau

$mock = Mockery::mock('\Path\To\Class',[$var1, $var2]);
shouldReceive()

Sau khi đã khởi tạo được Mock Object, đây sẽ là hàm được ta sử dụng nhiều nhất. Khai báo shouldReceive($methodName) sẽ chỉ định những hàm nào mà Mock Object của ta sẽ gọi. Ví dụ

$mock->shouldReceive(all)

chỉ ra rằng, Mock object của ta sẽ có thể gọi tớ method all()

Chỉ định số lần gọi

Sau khi đã chỉ ra Mock Object của ta sẽ bao gồm những method nào, ta có thể chỉ định thêm, các method đó sẽ được gọi bao nhiêu lần với once() , twice() hoặc times($number . Ví dụ như, khi ta muốn kiểm tra hàm tính trung bình số view của nhiều bài post, ta có thể tạo Mock Object cho post, và chỉ định nó gọi đến phương thức lấy số view 5 lần, ta có thể làm như sau

$mock = Mockery('Repositories\PostRepository');
$mock->shouldReceive('countView')->times(5)
Chỉ định tham số cho method

Và tất nhiên, trong đa số các trường hợp, các method của ta sẽ cần nhận được tham số truyền vào. Trong trường hợp đó, ta cần chỉ định các tham số đó với hàm with() . Cần lưu ý rằng, nếu bạn khai báo tham số trong with() khác biệt so với khi gọi thực tế, lỗi có thể xảy ra. Trở lại ví dụ bên trên, nếu như bạn khai báo

$mock->shouldReceive('countView')->with('php')->times(5);

nhưng trong controller, lại gọi đến

$this->repository->countView('js');

Test của bạn sẽ báo lỗi ngay

Chỉ định kết quả

Cuối cùng, việc quan trọng nhất là , ta cần phải chỉ định ra kết quả trả về. Thực hiện điều này với hàm andReturn. Trở lại ví dụ bên trên, giả sử như, ta muốn hàm countView của mình , khi chạy 5 lần, kết quả trả về lần lượt là 5,6,7,8,9 , có thể viết

$mock->shouldReceive('countView')->with('php')->times(5)->andReturn(5,6,7,8,9);

Như vậy, tới đây, ta đã có thể test được chức năng tính trung bình view một cách hoàn toàn độc lập với chức năng đếm số view. Không cần phải biết, liệu hàm countView() đã đúng chưa, hay thậm chí là đã đc viết hay chưa, bằng cách dùng mock object và mô phỏng lại hoạt động của hàm đó, ta đã có thể test chức năng tính trung bình của mình một cách hoàn toàn độc lập . Code đầy đủ cho phần test sẽ có dạng như

    pblic function testAverageView()
    {
        $mock = Mockery::mock('PostRepository')->makePartial();
        $mock->shouldReceive('countView)->with('php')->times(5)->andReturn(5,6,7,8,9);
        $result = $mock->averageView();
        $this->assertEquals(7,$result);
    }
makePartial

Ta đã cùng đi qua các bước cơ bản để thực hiện việc tạo Mock Objec và mock các chức năng. Nhưng nếu như tất cả các hàm ta dự định sử dụng trong khi test đều phải khai báo đầy đủ như vậy, công việc ta phải làm sẽ nhiều hơn rất nhiều. Đó là lúc ta cần sử dụng đến partial mock. Giống như tên gọi, partial mock là khi, ta sẽ chỉ tạo mock cho 1 phần cụ thể, còn những chức năng còn lại, ta vấn sử dụng như bình thường của object. Có hai cách làm, hoặc bạn thể có thể khai báo chỉ những chức năng nào của object được mock ngay từ khi khởi tạo

$mock = Mockery('Repositories\PostRepository[save,all]');

hoặc khai báo partial cho mock object sau khi đã khởi tạo

$mock->makePartial();

Với cách làm thứ hai, chỉ những phương thức nào được ta khai báo mock ( bằng cách dùng shouldReceive ) sau đó mới được thực hiện mock, còn những hàm khác của object vẫn được gọi như bình thường.