[Unit test] PHP Testing Interview 2

Bài viết tổng hợp 1 số note để testing với

  • Một vài packages bạn có thể dùng trong unit test.
  • Thao tác Database.
  • Private/Protected attributes, methods.

Me: Laravel có hỗ trợ Package nào làm việc với UnitTest không nhỉ?

PHP Testing: Có một vài phương thức sau:

Me: Bạn có thể ví dụ cho tôi không?

PHP Testing: Chắc đa số chúng ta sẽ sử dụng package Mockery. Có thể nói Stub và Mock là 2 khái niệm Test cô lập được sử dụng nhiều nhất trong phần unit test.

Khi mock 1 object bạn nên nhớ

PHPUnit cung cấp một phương thức hữu ích đó là getMockBuilder(), nó cho phép bạn tạo 1 class mới thoả mãn điều kiện trên ngay khi đang chạy test mà không phải tạo file mới cho mỗi class.

Và tất cả các methods của mock object này đều trả về null. Do đó nếu bạn sử dụng methods nào cần stub lại

Tuy nhiên, có 1 ngoại lệ đó là tất cả method của mock object đều trả về null. Những method này được gọi là stubs!

Stub method là một method bắt chước hành vi của method ban đầu theo 2 tiêu chí: cùng tên và cùng parameters. Điểm khác biệt của stub method là tất cả code logic bên trong sẽ bị loại bỏ (chỉ trả về giá trị cuối cùng bạn mong đợi

Như vậy: Bây giờ bạn đã có thể overridden giá trị trả về của 1 method bằng stub method bên trong unit test. Sử dụng mock và stub kết hợp để chạy test case và test Dependency Injection

Góc nhìn cá nhân về Dependency Injection:

  • DI là cách viết code ngoài các ưu điểm khác thì nó còn để có thể testing được, khi đó code giảm bớt sự phụ thuộc. Chi tiết đọc phần “Nhưng tại sao sử dụng dependency injection” tại https://viblo.asia/p/php-unit-test-501-su-dung-mock-objects-stub-methods-va-dependency-injection-YWOZryg7KQ0. Tóm tắt lại ý đó thì code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public function processPayment(array $paymentDetails)
    {
    $transaction = new AuthorizeNetAIM(self::API_ID, self::TRANS_KEY);
    $transaction->amount = $paymentDetails['amount'];
    $transaction->card_num = $paymentDetails['card_num'];
    $transaction->exp_date = $paymentDetails['exp_date'];

    $response = $transaction->authorizeAndCapture();

    if ($response->approved) {
    return $this->savePayment($response->transaction_id);
    }

    throw new Exception($response->error_message);
    }
    Sẽ không thể test được mà cần chuyển về DI
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public function processPayment(AuthorizeNetAIM $transaction, array $paymentDetails)
    {
    $transaction->amount = $paymentDetails['amount'];
    $transaction->card_num = $paymentDetails['card_num'];
    $transaction->exp_date = $paymentDetails['exp_date'];

    $response = $transaction->authorizeAndCapture();

    if ($response->approved) {
    return $this->savePayment($response->transaction_id);
    }

    throw new Exception($response->error_message);
    }
  • Khi test DI, bạn tự khởi tạo mock object, stub method mà bạn dùng rồi sau đó truyền nó vào các class cần test. Một tùy chọn khác là DI qua bind() hoặc singleton() mà cái này ít dùng.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
     public function testProcessPaymentReturnsTrueOnSuccessfulPayment()
    {
    $paymentDetails = [
    'amount' => 123.99,
    'card_num' => '4111-1111-1111-1111',
    'exp_date' => '03/2013',
    ];

    $payment = new Payment();

    $response = new stdClass();
    $response->approved = true;
    $response->transaction_id = 123;

    // Mock object nào
    $authorizeNet = $this->getMockBuilder(AuthorizeNetAIM::class)
    ->setConstructorArgs([Payment::API_ID, Payment::TRANS_KEY])
    ->getMock();

    // Stub method `authorizeAndCapture`
    $authorizeNet->expects($this->once())
    ->method('authorizeAndCapture')
    ->will($this->returnValue($response));

    // Truyền DI $authorizeNet sau khi đã giả lập thành công.
    $result = $payment->processPayment($authorizeNet, $paymentDetails);

    $this->assertTrue($result);
    }
    Ngoài ví dụ trên, sau khi mock object, bạn có thể tìm thấy
  • Stub without Mockery: https://github.com/tuanpht/laravel-test-example/pull/1/files#diff-aac49332ac4c101b14e9115b7a49b742R13-R16
  • Stub with Mockery: https://github.com/tuanpht/laravel-test-example/pull/1/files#diff-162143d3f08dc0e12b06a6871e8c4f8fR14-R17

Me: Vậy còn việc test các thuộc tính, methods có phạm vi private/protected thì sao?

PHP Testing: Khi Test các private và protected methods, properties, bạn cần sử dụng Reflection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Get private/protected property value
* $this->assertEquals('views/home', $this->getObjectProperty($view, 'file_name'));
*/
public function getObjectProperty($object, $propertyName) {
$reflector = new \ReflectionClass($object);
$property = $reflector->getProperty($propertyName);
$property->setAccessible(true);

return $property->getValue($object);
}

/**
* Call protected/private method of a class.
* $this->invokeObjectMethod($view, 'getData');
*/
public function invokeObjectMethod($object, $methodName, $parameters = [])
{
$reflection = new \ReflectionClass($object);
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);

return $method->invokeArgs($object, $parameters);
}

Me: Hmm, ngon quá nhỉ, vậy còn khi Test với Database?

PHP Testing: Để test với các thao tác database bạn cần làm 2 việc

  • Tạo một 1 database mới không liên quan gì đến database của môi trường dev: có thể là MySQL hoặc SQLite: nhỏ gọn, nhanh chóng. SQLite nhỏ gọn, xử lý nhanh nhưng nó chỉ thích hợp với các ứng dụng chỉ có thao tác CRUD, với các ứng dụng thực tế cần JOIN, trigger thì nó không sử dụng được, vì vậy, chắc hẳn trên thực tế sẽ sử dụng MySQL hơn là SQLite https://freetuts.net/gioi-thieu-sqlite-sqlite-la-gi-1719.html

Đây mới chỉ là phần chọn drive nào. Vậy phần tạo database (migrate) đâu?

Laravel hỗ trợ ta làm việc này với trait RefreshDatabase: https://github.com/tuanpht/laravel-test-example/blob/master/tests/Integration/SetupDatabaseTrait.php
Trai này thực hiện luôn cả migration

  • Trỏ tới Database đó
    Việc trỏ tới database đó sẽ là việc thay đổi môi trường, quyết định trong file config:
    1
    2
    3
    4
    5
    <php>
    <server name="APP_ENV" value="testing"/>
    <!-- APP_KEY for integration http test -->
    <server name="APP_KEY" value="base64:HaoSf5Y02/vR1a1WGy3qfQ/iZhON6PsLkF8QOBr8RyA= " />
    </php>
    https://github.com/tuanpht/laravel-test-example/blob/master/phpunit.dist.xml#L26.

Lúc này bạn cần thêm file cấu hình khi testing với định dạng .env.testing

In addition, you may create a .env.testing file in the root of your project. This file will override the .env file when running PHPUnit tests or executing Artisan commands with the --env=testing option.

Nếu bạn sử dụng SQLite thì bạn cần thêm phần này
Bạn sẽ config trong file như vậy https://github.com/tuanpht/laravel-test-example/pull/2/files#diff-1ef70a974f238d54faeb90eb02582e90R31-R36

1
2
3
4
5
6
7
8
<!-- Using SQLite in memory database test -->
<server name="DB_CONNECTION" value="sqlite" />
<server name="DB_DATABASE" value=":memory:" />
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>

Tài liệu tham khảo:

Slide:

Docs:

Demo:

Sun Asterisk Training Project:

Author

Ming

Posted on

2020-01-25

Updated on

2021-12-07

Licensed under

Comments