Ở phần trước, mình đã giới thiệu cách cài đặt cũng như cách tổ chức và viết các test cases đơn giản. Trong phần này, mình sẽ giới thiệu các kỹ năng viết unit test phức tạp hơn.
Một câu hỏi mà chúng ta thường xuyên đặt ra là: “phải viết unit test đến bao giờ?”
Câu trả lời thường thấy là: “Cho tới khi đạt 100% code coverage”.
Vậy code coverage là gì?
In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage. — Wikipedia
Bạn có thể hiểu là nó là độ bao phủ của phần test của mình. Thông số này càng lớn nghĩa
là chương trình được test càng kỹ và sẽ ít bug hơn.
Làm thế nào để biết được code coverage bây giờ? Phần nào là phần chưa được test đến?
PHPUnit cũng cung cấp cho bạn chức năng phân tích code coverage của phần script test của các bạn.
Tính năng này yêu cầu bạn phải cài Xdebug và hai extensions là tokenizer và xmlwriter
Bạn chạy phpunit với option tạo code coverage report
1
$ ./vendor/bin/phpunit --coverage-text
Và bạn thu được kết quả như sau:
PHPUnit report code coverage with text format
Bạn cũng có thể tạo report ra file với các optíon coverage-text
, coverage-html
,
coverage-php
, coverage-xml
. Bạn có thể tham khảo thêm tại đây
Để đạt tới code coverage tối đa (100%) thì bạn cần viết cả test cho các private và protected methods. Vậy thì làm thế nào để viết unit test cho chúng trong khi chúng ta không thể call chúng từ ngoài class của chúng?
Có thể thay đổi code để cho phép test kết quả của các protected method hay giá trị
của property, nhưng ở đây mình sẽ giới thiệu một cách test các private và protected
methods mà mình vẫn hay dùng, đó là sử dụng extension reflection.
Đầu tiên bạn thêm ba methods sau vào class test của bạn.
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
class MyTestCase extends \PHPUnit_Framework_TestCase
{
/**
* Get a non public property of an object
*
* @param object $obj
* @param string $property
* @return mixed
*/
protected function getNonPublicProperty($obj, $property)
{
if (!is_object($obj) || !is_string($property)) {
return null;
}
$ref = new \ReflectionProperty(get_class($obj), $property);
$ref->setAccessible(true);
return $ref->getValue($obj);
}
/**
* Set value for a non public property of an object
*
* @param object $obj
* @param string $property
* @param mixed $value
*/
protected function setNonPublicProperty($obj, $property, $value)
{
if (!is_object($obj) || !is_string($property)) {
return;
}
$ref = new \ReflectionProperty(get_class($obj), $property);
$ref->setAccessible(true);
$ref->setValue($obj, $value);
}
/**
* Call protected/private method of a class.
*
* @param object $obj Instantiated object that we will run method on.
* @param string $method Method name to call
* @param array $params Array of parameters to pass into method.
* @return mixed Method return.
* @throws \InvalidArgumentException
*/
protected function invokeNonPublicMethod($obj, $method, $params = [])
{
if (!is_object($obj) || !is_string($method) || method_exists($obj, $method)) {
throw new \InvalidArgumentException();
}
$ref = new \ReflectionMethod($obj, $method);
$ref->setAccessible(true);
return $ref->invokeArgs($obj, $params);
}
}
Bây giờ thì bạn có thể dễ dàng test các private/protected methods và property rồi phải không?
Ví dụ để test protected method cryptPassword
của class User thì chúng ta làm như sau:
1
2
3
4
5
6
7
8
<?php
// ...
public function testCryptPassword($expectedCryptedPassword, $rawPassword)
{
$user = new User();
$result = $this->invokeNonPublicMethod($user, 'cryptPassword', [$rawPassword]);
$this->assertSame($expectedCryptedPassword, $result);
}
Vậy là chúng ta có thể viết test đến 100% code coverage rồi phải không nào?
Nhưng liệu như vậy đã đủ chưa?
Theo cá nhân mình là chưa đủ. Bởi vì code coverage chỉ cho thấy tất cả các phần code
đã được test chạy qua, nhưng chưa phải là đã test được hết tất cả các trường hợp.
Ví dụ như truờng hợp sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
// ...
/**
* Return 'XY' when $x >= 0 and $y >= 0
* Return 'xY' when $x < 0 and $y >= 0
* Return 'Xy' when $x >= 0 and $y < 0
* Return 'xy' when $x < 0 and $y < 0
*
* @param int $x
* @param int $y
* @return string
*/
public function abc ($x, $y)
{
$z = '';
$z .= ($x >= 0) ? 'X' : 'x';
$z .= ($y >= 0) ? 'Y' : 'y';
if ($x < 0 && $y < 0) {
$z = 'XY';
}
return $z;
}
Như với method trên, ta thấy chỉ với 3 truờng hợp (1, 1), (-1, 1) và (1, -1),
nó đã cho code coverage 100% và test là pass. Nhưng với truờng hợp (-1, -1) nó lại sai! @@
Vì vậy chúng ta nên tạo matrix test để có thể bao phủ hết tất cả các truờng hợp.
Trong một số trường hợp, chúng ta phải cover cả dữ liệu ngẫu nhiên, dữ liệu time()
,
hay từ hãng thứ ba, vậy thì làm thế nào mình có thể test được các case theo
mong muốn của mình chứ?
Comments