PHPUnit – Bạn đã hiểu đúng về Stub, Mock, Spy, Fake, Dummy chưa?

Bài viết này sẽ trình bày về các khái niệm Stub, Mock, Spy, Fake và Dummy trong việc viết UnitTest. Thường thì các khái niệm này thường khiến mọi người nhầm lẫn với nhau. Khi cần sử dụng, mọi người thường tạo ra các đối tượng Mock mà ít quan tâm đến các quy tắc cụ thể của chúng và không biết áp dụng chúng trong trường hợp nào. Dù việc này không ảnh hưởng đến chất lượng sản phẩm, nhưng nó gây ra một số phiền toái trong quá trình phát triển sản phẩm. Một số người có thể cho rằng việc kiểm thử không tốn nhiều tài nguyên nên không quan trọng lắm, vì chỉ có các nhà phát triển mới làm việc với chúng. Tuy nhiên, việc viết test đúng cách cũng là một cách để đánh giá trình độ của một lập trình viên. Nếu chúng ta chú ý thêm một chút để viết code tốt hơn và ngắn gọn hơn, tại sao lại không làm điều đó?

Tại sao chúng ta cần chúng?

Trước tiên, hãy nhắc lại một chút về lý thuyết. Mọi người thường gọi chung các đối tượng này là Mock, nhưng thực tế thì tên chuẩn của chúng là Test Double. Vậy Test Doubles được sử dụng để làm gì?

Code thường có tính phụ thuộc, không có chức năng nào gói gọn trong một đơn vị, mà nó phụ thuộc vào các đoạn code khác và thậm chí phụ thuộc vào những thành phần không nằm trong code của chúng ta như database, biến toàn cục hoặc http request,… Những thành phần như vậy mà một đơn vị cần sử dụng được gọi là dependency. Chúng ta không thể kiểm soát 100% các dependencies này có hoạt động như kỳ vọng hay không? Việc kiểm thử các dependencies thường rất khó khăn do chúng phụ thuộc vào các đoạn code nằm ngoài dự đoán hoặc thậm chí có rất nhiều trường hợp là bất khả thi (khi internet bị trục trặc, môi trường bị lỗi, …).

Để kiểm thử các đơn vị phụ thuộc vào các dependencies khác, bạn cần giả định được trạng thái của các dependencies này để đảm bảo rằng Input hoặc Output của chúng nằm trong phạm vi tính toán của bạn. Một trong những phương án phổ biến để làm điều này là sử dụng Dependency Injection.

Ví dụ:

Kết quả trả về từ một request từ API luôn là một chuỗi JSON định sẵn {key: value}

vardump($this->httpRepository->getRequest()); // Kết quả phải luôn là // {key: value}

Test Doubles chính là các đối tượng giả định như vậy và chúng không cần thể hiện đầy đủ chức năng của một đối tượng thật.

Về cơ bản, chúng ta có 5 loại doubles:

  • Dummy Object
  • Test Stubs
  • Test Spies
  • Test Mocks
  • Test Fakes

PHPUnit cung cấp phương thức chính thức để tạo Dummy, Stubs và Mocks. Thay vào đó, Spies có thể được tạo ra thông qua các mocking API trong khi Test Fakes thì không. Về khái niệm, Test Fakes và Test Stub khá giống nhau. Nếu bạn muốn hiểu chi tiết hơn, hãy xem tài liệu tại đây: https://martinfowler.com/articles/mocksArentStubs.html

Dummy Object

Đây là loại test double đơn giản nhất. Chúng ta sử dụng Dummy Object khi chỉ cần giả định một dependency để đủ đối số mà không quan tâm đến chức năng của nó.

Hãy xem ví dụ sau:


<?php
/**
 * Baz
 */
class Baz {
    public $foo;
    public $bar;

    public function __construct(Foo $foo, Bar $bar) {
        $this->foo = $foo;
        $this->bar = $bar;
    }

    public function processFoo() {
        return $this->foo->process();
    }

    public function mergeBar() {
        if ($this->bar->getStatus() == 'merge-ready') {
            $this->bar->merge();
            return true;
        }
        return false;
    }
}
?>

Để kiểm thử các đơn vị trong lớp Baz, chúng ta phải truyền đối tượng Foo và Bar vào hàm __construct() của Baz.

```php
<?php
declare(strict_types=1);
use PHPUnitFrameworkTestCase;

/**
 * @covers Bar
 */
final class BarTest extends TestCase {
    public function testMergeBarCorrectly() {
        $foo = $this->getMockBuilder('Foo')->getMock();
        $bar = $this->getMockBuilder('Bar')->setMethods(['getStatus', 'merge'])->getMock();
        $bar->expects($this->once())->method('getStatus')->will($this->returnValue('merge-ready'));

        $baz = new Baz($foo, $bar);

        $this->assertTrue($baz->mergeBar(), 'Baz::mergeBar did not correctly merge');
    }
}
?>

Trong trường hợp này, $foo đóng vai trò của Dummy Object. Test này không quan tâm $foo thực hiện nhiệm vụ gì, đơn giản nó chỉ cần có mặt để đủ đối số cho hàm __construct() của lớp Baz.

## Test Stubs

Một Stub là một đối tượng mà khi chúng ta gọi một phương thức nào đó của đối tượng đó, chúng ta định trước kết quả của phương thức đó. Trong ví dụ trên, $bar chính là một đối tượng Stub, mỗi khi phương thức getStatus() được gọi, nó sẽ trả về kết quả mà chúng ta đã quy định trước. Mặc định, PHPUnit không thể giả lập các phương thức protected và private, nếu muốn, bạn sẽ phải sử dụng Reflection API để tạo ra một bản sao của đối tượng cần giả lập và thiết lập lại phạm vi của phương thức (một số framework như Mockery hỗ trợ sẵn điều này).

```php
<?php
/**
 * Foo
 */
class Foo {
    protected $message;

    protected function bar($env) {
        $this->message = "PROTECTED BAR";

        if ($env == 'dev') {
            $this->message = "DEVELOPER BAR";
        }
    }
}
?>

Sử dụng Reflection để giả lập phương thức bar()

```php
<?php
declare(strict_types=1);
use PHPUnitFrameworkTestCase;

/**
 * @covers Foo
 */
final class FooTest extends TestCase {
    public function testProtectedBar() {
        $reflectedMethod = new ReflectionMethod('Foo', 'bar');
        $reflectedMethod->setAccessible(true);

        $foo = new Foo();
        $reflectedMethod->invoke($foo, 'dev');

        $this->assertAttributeEquals(
            'PROTECTED BAR',
            'message',
            $foo,
            'Did not get expected message'
        );
    }
}
?>

Khi viết test, bạn cũng có thể dựa vào số lượng các Stub như một dấu hiệu để biết code của bạn có tốt không? Khi số lượng các Stub tăng lên nhanh chóng, điều đó có nghĩa là đơn vị đó phụ thuộc quá nhiều vào các dependencies hoặc đơn vị đó đang thực hiện quá nhiều nhiệm vụ. Điều này đồng nghĩa với việc đã đến lúc xem xét tái cấu trúc code.

PHPUnit hỗ trợ thiết lập các kỳ vọng đối với Stub khi thực thi kiểm thử. Các kỳ vọng này có thể là số lần phương thức được gọi, thứ tự các phương thức được gọi, kết quả trả về dựa trên Input đầu vào. Để biết thêm chi tiết, hãy tham khảo tài liệu PHPUnit (PHPUnit documentation).

## Test Spies

Spy là một đối tượng Stub nhưng không giả lập kết quả trả về. Điều này có nghĩa là bạn chỉ quan tâm đến việc phương thức đó có được gọi hay không và được gọi bao nhiêu lần, chứ không quan tâm đến kết quả của nó.

```php
<?php
/**
 * Alpha
 */
class Alpha {
    protected $beta;

    public function __construct($beta) {
        $this->beta = $beta;
    }

    public function cromulate($deltas) {
        foreach ($deltas as $delta) {
            $this->beta->process($delta);
        }
    }
}
?>

Lớp kiểm thử không quan tâm đến giá trị trả về của phương thức process()

```php
<?php
declare(strict_types=1);
use PHPUnitFrameworkTestCase;

/**
 * @covers Alpha
 */
final class AlphaTest extends TestCase {
    public function testExpectedTimesBetaProcessCalled() {
        $beta = $this->getMockBuilder('Beta')->setMethods(['process'])->getMock();
        $beta->expects($this->at(2))->method('process');

        $deltas = ['Hello', 'Ohaiyo Gozaimasu', 'Xin chao'];

        $alpha = new Alpha($beta);
        $alpha->cromulate($deltas);
    }
}
?>

## Đối tượng Mock

Mock khác biệt so với các loại test double khác, đối tượng này hoạt động giống như phương thức thực sự mà nó giả lập. Nói cách khác, một mock sẽ thực sự chạy và bạn không thể quy định kết quả trả về cho nó. Đối tượng Mock thường được sử dụng khi bạn muốn kiểm tra một chức năng, có các phương thức liên quan với nhau hoặc trong trường hợp bạn muốn kiểm tra một số khẳng định khi phương thức thực thi.

Hãy xem ví dụ sau: Viết một chương trình demo đơn giản, nếu người dùng nhập mật khẩu đúng thì cho phép truy cập, ngược lại thì in ra thông báo lỗi và kết thúc chương trình.

```php
<?php
/**
 * Auth
 */
class Auth {
    protected $user;

    public function __construct($user) {
        $this->user = $user;
    }

    public function authorize($password) {
        if ($this->checkPassword($password)) {
            return true;
        }

        return false;
    }

    protected function checkPassword($password) {
        if (empty($this->user['password']) || $this->user['password'] !== $password) {
            echo 'You dont have permission';
            $this->callExit();
        }

        return true;
    }

    protected function callExit() {
        exit;
    }
}
?>

Ở đây, tôi tạo ra một đối tượng Mock giả lập lớp Auth, đặc biệt là đối tượng này có một phương thức giả định là exit(). Các phương thức khác sẽ hoạt động như bình thường, riêng phương thức exit() sẽ trở thành một phương thức trả về null để đảm bảo rằng các kiểm thử đơn vị vẫn hoạt động trơn tru thay vì bị dừng ngay lập tức khi gọi phương thức exit(). Khi gọi $mockObject->authorize($password), code sẽ chạy như khi thực thi chương trình thực tế và trả về thông báo "Bạn không có quyền truy cập".

```php
<?php
declare(strict_types=1);
use PHPUnitFrameworkTestCase;

/**
 * @covers Auth
 */
final class AuthTest extends TestCase {
    public function testAuthorizeWhenWrongPassword() {
        $user = array('username' => 'kopitop');
        $password = 'wrongpassword';

        $mockObject = $this->getMockBuilder('Auth')
            ->setConstructorArgs(array($user))
            ->setMethods(array('callExit'))
            ->getMock();

        $mockObject->expects($this->once())->method('callExit');

        $this->expectOutputString('You dont have permission');

        $mockObject->authorize($password);
    }
}
?>

## Kết luận

PHPUnit cung cấp 4 loại test double, mỗi loại được sử dụng trong các trường hợp khác nhau, đảm bảo rằng code ngắn gọn và sử dụng ít tài nguyên nhất có thể. Việc tạo ra một đối tượng Mock đầy đủ hành vi cho một kiểm thử chỉ sử dụng Dummy Object nhanh chóng và nhẹ nhàng sẽ là sai lầm. Trong các dự án lớn, có thể có hàng trăm hoặc nhiều hơn số lượng kiểm thử và việc chờ đợi để chạy kiểm thử không phải lúc nào cũng vui vẻ khi code đã chạy ổn định. Vì vậy, việc tối ưu lại cấu trúc code là điều cần thiết.

## Tham khảo

[The Grumpy Programmer's PHPUnit Cookbook - Chris Hartjes](https://martinfowler.com/articles/mocksArentStubs.html)
[PHPUnit documentation](https://phpunit.de/manual/current/en/test-doubles.html)
[PHP Reflection documentation](http://php.net/manual/en/book.reflection.php)

**Paragraph edited by: [HEFC](https://www.hefc.edu.vn/)**

Related Posts

Xét nghiệm Giải phẫu bệnh – Dẫn đường cho việc điều trị

Xét nghiệm giải phẫu bệnh được thực hiện trên những mẫu bệnh phẩm tế bào, bệnh phẩm mô từ các cơ quan trong cơ thể được sinh…

Phương pháp điều trị tủy răng tại nha khoa hiện nay

Viêm tủy răng là một trong những vấn đề về sức khỏe răng miệng nghiêm trọng. Người mắc viêm tủy răng không chỉ phải chịu đựng những…

Mỹ thuật ứng dụng là gì? (cập nhật 2023)

Khi những giá trị thẩm mỹ ngày càng được chú trọng thì các phẩm mỹ thuật ứng dụng ngày càng đi sâu vào đời sống của mọi…

Bát quái đồ là gì? Ý nghĩa và vai trò của bát quái trong phong thủy

Bát quái đồ là vật phẩm phong thủy được sử dụng khá rộng rãi và phổ biến trong văn hoá phương Đông, nhằm mang lại những niềm…

Du học ngành khoa học ứng dụng và cơ bản

>> Du học ngành khoa học đại cương >> Các trường có đào tạo ngành Khoa học ứng dụng và cơ bản Khoa học Ứng dụng và…

Trồng răng implant là gì? Những điều cần phải biết trước khi chọn trồng răng implant

Trồng răng implant là phương pháp trồng răng cấy trụ kim loại vào xương hàm để thay thế cho răng đã mất. Chính vì vậy trụ implant…