[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:
- Mặc định PHP sẽ có sẵn PHPUnit https://phpunit.readthedocs.io/en/8.5/.
- Ngoài ra bạn có thể sử dụng packge Mockery thêm cho test hiệu quả https://github.com/mockery/mockery.
- Laravel cũng cung cấp thêm 1 vài phương thức để test bạn có thể tham khảo: https://laravel.com/docs/6.x/testing
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ì codeSẽ 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
15public 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);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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ặcsingleton()
mà cái này ít dùng.Ngoài ví dụ trên, sau khi mock object, bạn có thể tìm thấy1
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
29public 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);
} - 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 Reflection1
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:https://github.com/tuanpht/laravel-test-example/blob/master/phpunit.dist.xml#L26.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>
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-R361
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:
- https://github.com/tuanpht/laravel-test-example/blob/master/docs/testing.md
- Viblo: https://viblo.asia/s/php-unit-testing-with-phpunit-Wj53OmBb56m
- https://github.com/mockery/mockery
Demo:
Sun Asterisk Training Project:
[Unit test] PHP Testing Interview 2
http://yoursite.com/2020/01/25/Unit-test-PHP-Testing-Interview-2/