UNIT TESTING TUTORIAL: PRIVATE/PROTECTED METHODS, COVERAGE REPORTS VÀ C.R.A.P

10/09/2021 22
unit testing tutorial iii banner
CODEWELL

Ở các phần trước, chúng ta đã biết một số khái niệm cơ bản về PHPUnit, cũng như cách viết một kiểm thử có ích và @dataprovicer. Tuy nhiên, các bài viết chưa có ví dụ nào về phương thức private/protected. Vì thế, hãy cùng tìm hiểu về nó trong phần 3 của loạt bài, Unit Testing Tutorial: Private/Protected methods, Coverage Reports và C.R.A.P.

Các bạn có thể tìm đọc các bài trước tại đây:

  1. UNIT TESTING TUTORIAL: LÀM QUEN VỚI PHPUNIT
  2. UNIT TESTING TUTORIAL: VIẾT MỘT KIỂM THỬ CÓ ÍCH VÀ @dataprovider

I. Kiểm thử theo phương thức private/protected

Ở các phần trước, chúng ta đã đề cập đến kiểm thử với phương thức public. Câu hỏi đặt ra làm thế nào viết kiểm thử cho private/protected methods vì khi khởi tạo, các class thường được kiểm tra thông qua lệnh new nên ta không thể truy cập phương thức này thông qua đối tượng được khởi tạo là $url->somePtotectedMethod().

Thông thường ta sẽ không trực tiếp kiểm thử private/protected. Có thể các phương thức public của class sẽ tương tác với phần này, vì những thứ không public sẽ chỉ truy cập được trong phạm vi class. Nếu xét như vậy thì thực tế là chúng ta vẫn đang gián tiếp kiểm thử các phương thức private/protected.

Vậy thì điều gì xảy ra nếu ta đang kiểm tra một abstract class với các phương thức private/protected, nhưng không thực sự tương tác với nó? Điều gì xảy ra khi ta muốn kiểm thử các case khác nhau cho một phương thức cụ thể nhưng không có cơ hội thực hiện phương thức public?

1. Class User đơn giản

Tạo một file app/Models/User.php như sau đây

Lưu ý: Không được sử dụng sha1() hoặc md5() cho phần mật khẩu. 

<?php
namespace App\Models;

class User
{

const MIN_PASS_LENGTH = 6;

private $user = array();

public function __construct(array $user)
{

$this->user = $user;

}

public function get()
{

 return $this->user;

}

public function setPassword($password)
{

if (strlen($password) < self::MIN_PASS_LENGTH)
{

return false;

}

$this->user['password'] = $this->crypt($password);

return true;

}

private function crypt($password)
{
return sha1($password);
}

}

 

Phần kiểm thử sẽ thực hiện trên class User bằng cách sử dụng $user = new User($details); 

Ta có thể truy cập phương thức setPassword() nhưng không thể truy cập phương thức crypt(), tuy nhiên điều đó không quá quan trọng. Với đoạn code này, ta sẽ thấy phương thức public tương tác với phương thức private, điều đó đồng nghĩa với việc phương thức private cũng đã được kiểm thử thành công. 

Phương thức public setPassword() gọi phương thức private crypt(), vì thế ta viết kiểm thử thông qua phương thức này.

2. Tiến hành tạo kiểm thử.

Tạo một class test rỗng tests/Models/UserTest.php

<?php

    namespace Tests\Models;

    use PHPUnit\Framework\TestCase;

 

    class UserTest extends TestCase

    {

        // todo

    }

 

Chạy bộ kiểm thử ta có được một cảnh báo như sau

unit testing tutorial iii 1

Như vậy, PHPUnit đã chạy kiểm thử này và sẵn sàng cho mã thực tế.

Hãy xác định những gì bạn cần kiểm thử trước khi đi đến các bước tiếp theo. Vì App\Models\User là một class đơn giản, ta có thể nhanh chóng thấy hai tình huống.

  1. setPassword() trả về true khi mật khẩu được set.
  2. getUser() trả về array user, trong đó sẽ chứa mật khẩu mới dùng để so sánh kết quả mong đợi.

Ta sẽ bắt đầu với phương thức setPassword() trả về true. Tạo phương thức rỗng.

<?php

    ...

    public function testSetPasswordReturnsTrueWhenPasswordCompletedSet()

    {

        //

    }

 

Trước khi khởi tạo đối tượng, hãy xác định 1 tham số cho hàm __costruct trong class User.

<?php

namespace Tests\Models;

use PHPUnit\Framework\TestCase;

use App\Models\User;

class UserTest extends TestCase

{

    public function testSetPasswordReturnsTrueWhenPasswordCompletedSet()

    {

        $details = array();

 

        $user = new User($details);

    }

}

 

Lưu ý: thêm một khai báo use.

Sau đó, chúng ta định nghĩa tham số cho setPassword() và gọi nó.

<?php

    ...

    public function testSetPasswordReturnsTrueWhenPasswordCompletedSet()

    {

        $details = array();

 

        $user = new User($details);

 

        $password = 'thanhtv';

 

        $result = $user->setPassword($password);

 

    }

 

Kết quả của $result mà ta cần sẽ là true, ta sẽ sử dụng xác nhận assertTrue(); để hoàn thành kết quả test như bên dưới

<?php

    ...

    public function testSetPasswordReturnsTrueWhenPasswordCompletedSet()

    {

        $details = array();

         $user = new User($details);

         $password = 'thanhtv';

         $result = $user->setPassword($password);

         $this->assertTrue($result);

    }

 

Khi chạy kiểm thử, kết quả sẽ hiện màu xanh lá như mong muốn!

Tiếp theo, chúng ta sẽ kiểm tra xem mật khẩu của mình có được mã hóa sha1 hay không? Ở đây, ta sẽ sử dụng phương thức get(), phương thức một dòng rất dễ kiểm thử. Đây là phương thức có quyền truy cập vào thuộc tính private $user. Ta sẽ tiến hành xác minh $user là thuộc tính có mật khẩu được mã hóa chính xác sha1.

Bằng cách kiểm thử phương thức get() chúng ta sẽ có thể gián tiếp kiểm thử __construct(), setPassword(), crypt().

Ở bài viết này, ta sẽ tiến hành kiểm thử xem password tạo ra có đúng như kỳ vọng hay không. Đầu tiên ta thiết lập user như giống như testSetPasswordReturnsTrueWhenPasswordCompletedSet() 

<?php

    public function testGetReturnsUserWithExpectedValues()

    {

        $details = array();

        

        $user = new User($details);

        

        $password = 'thanhtv';

        

        $user->setPassword($password);

    }

 

Ta sẽ giả định kết quả của setPassword() đã pass ở trường hợp này. Nếu chưa pass thì ta sẽ dễ dàng nhận ra ngay ở các bước tiếp theo. Theo ví dụ trên đây, ta có thể hiểu phần mật khẩu chưa mã hóa là “thanhtv”. Ta cũng biết rằng qua phương thức setPassword() sử dụng sha1 để mã hóa. Do đó ta có thể xác định được kết quả mong đợi thực sự là:

$expectedPasswordResult = '5f133db68b1f13b29ee85337829de400bf4918bf';

Sau đấy, ta gọi phương thức get(); để lấy thông tin user ở trạng thái hiện tại.

$currentUser = $user->get();

Ta sẽ cần get() trả về một array, và so sánh index password với $expectedPasswordResult. Vì thế ta dùng khẳng định assertEquals(). Tới đây là kiểm thử đã hoàn thành.

OK (10 tests, 10 assertions)

 

3. Kiểm thử trực tiếp private/protected methods

Điều gì xảy ra nếu ta muốn viết kiểm thử nhiều hơn cho các phương thức private/protected (private/protected methods)? Hoặc là ta không muốn kiểm thử chúng thông qua phương thức public, thay vào đó ta chỉ muốn tương tác trực tiếp với phương thức private/protected? PHP cung cấp cho ta 1 phương thức tuyệt vời để làm điều này, đó là invokeMethod().

Trong Tests\Models\UserTest ta thêm vào phương thức invokeMethod() như sau:

<?php

    ...

    /**

     * Call protected/private method of a class.

     *

     * @param object &$object    Instantiated object that we will run method on.

     * @param string $methodName Method name to call

     * @param array  $parameters Array of parameters to pass into method.

     *

     * @return mixed Method return.

     */

 public function invokeMethod(&$object, $methodName, array $parameters = array())

    {

        $reflection = new \ReflectionClass(get_class($object));

        $method = $reflection->getMethod($methodName);

        $method->setAccessible(true);

 

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

    }

 

Sử dụng invokeMethod(); đơn giản như sau:

<?php

   ...

   $this->invokeMethod($user, 'crypt', array('thanhtv'));

 

điều trên tương đương với việc gõ:

<?php

    ...

    $user->crypt('thanhtv');

 

Nếu có nhiều class muốn kiểm thử trực tiếp bằng phương thức private/protected, ta nên đặt invokeMethod() vào một lớp cha và các lớp kiểm thử kế thừa lớp cho đó.

 

II. Báo cáo kiểm thử: Coverage Reports

Thực tế trong các dự án thật, lượng mã nguồn cũng như lượng mã kiểm thử là rất lớn. Chúng ta có thể nói tất cả các mã nguồn của mình đã được kiểm thử, nhưng cần có một cơ sở để chắc chắn điều đó.

Bạn có thể duyệt qua từng kiểm thử để xác minh thủ công mọi đoạn code của bạn đã được kiểm thử. Tuy nhiên, việc này rất tốn công sức và thời gian, đồng thời nó có vẻ như là một công việc nhàm chán. May thay, PHPUnit có một công cụ khá mạnh để thay chúng ta làm điều này.

Công cụ tạo Coverage report tự động render ra các file báo cáo bằng html tĩnh. Ta có thể duyệt và xem số liệu thống kê về mã nguồn. Bao gồm số lượng mã nguồn đã được kiểm thử, mức độ phức tạp của mã nguồn.

Công cụ này còn cho bạn biết các rẽ nhánh nào trong mã nguồn của bạn chưa được kiểm thử, chẳng hạn như một rẽ nhánh của lệnh if.

1. Tạo một Coverage Report.

Để tạo một báo cáo, ta chỉ cần chuyển cờ –coverage-html cho PHPUnit, cùng với thư mục đích để tạo các file html. Ở đây ta sẽ dùng một thư mục reports.

$ vendor/bin/phpunit –coverage-html reports

Lưu ý: bạn phải enable x-debug cho PHP thì mới chạy được lệnh trên

Bây giờ nếu bạn nhìn vào cấu trúc thư mục của dự án ta có thêm thư mục reports được tạo và chứa các tập tin html. Mở index.html ta sẽ thấy:

unit testing tutorial iii 2

Nếu bạn sử dụng PHPUnit cũ hoặc mới hơn thì giao diện có khác đôi chút vì css có thể được thay đổi theo các phiên bản khác nhau.

2. Chỉ có Url và User class được hiện lên.

Ta có thể thấy PHPUnit rất thông minh. Có 3 files kiểm thử, và một trong số đó không được liệt kê ở đây là StupidTest.php. Đó là bởi vì thử nghiệm này không thực sự tương ứng với mã nguồn nào của dự án. Vì vậy PHPUnit không tạo ra một báo cáo cho nó.

Click chuột vào Url.php trong coverage report bạn sẽ thấy:

unit testing tutorial iii 3

Nhìn vào chú thích cuối trang ta thấy màu xanh lá là code đã được kiểm thử, màu đỏ là code chưa được kiểm thử và màu vàng là code đã chết. 

Trong ví dụ trên, tất cả các dòng code đều có màu xanh lá. Nếu bạn di chuyển chuột lên một trong các dòng trên một hộp thông tin nhỏ xuất hiện cho bạn biết các kiểm thử bao gồm dòng lệnh này.

unit testing tutorial iii 4

Đây là một đoạn mã rất đơn giản, và dễ hiểu, tuy nhiên nó cũng giúp chúng ta hình dung được những hoạt động về sau.

3. User class Coverage Report

Quay trở lại index.html page, bạn click chuột vào User.php page

unit testing tutorial iii 5

Nhìn vào báo cáo này ta thấy có một đoạn màu đỏ. Cụ thể, câu lệnh if bên trong phương thức setPassword() không phải lúc nào cũng trả về true, vì thế có đoạn code trong đấy chưa bao giờ được kiểm thử.

Đối với khai báo if trong tình huống này, nội dung trả về đơn giản là false. Trở lại với UserTest class, ta thêm một kiểm thử cho tình huống này.

<?php

   ...

   public function testSetPasswordReturnsFalseWhenPasswordLengthIsTooShort()

    {

        //

    }

 

Ở kiểm thử trước, ta chuyển đến setPassword() đoạn mật khẩu có 7 ký tự. Để kích hoạt khối if trên, ta truyền mật khẩu với độ dài nhỏ hơn 6 ký tự. Phần còn lại làm tương tự với ví dụ trước.

<?php

    ...

    public function testSetPasswordReturnsFalseWhenPasswordLengthIsTooShort()

    {

        $details = array();

        

        $user = new User($details);

        

        $password = 'thanh';

        

        $result = $user->setPassword($password);

    }

 

Trường hợp này, ta sẽ cần $result trả về kết quả false, vì thế ta sẽ dùng khẳng định assertFalse() cho tình huống này. Dưới đây là kết quả:

<?php

    ...

    public function testSetPasswordReturnsFalseWhenPasswordLengthIsTooShort()

    {

        $details = array();

 

        $user = new User($details);

 

        $password = 'thanh';

 

        $result = $user->setPassword($password);

 

        $this->assertFalse($result);

    }

 

Chạy bộ kiểm thử ta sẽ có OK (11 tests, 11 assertions)

Chạy lại lệnh: $ vendor/bin/phpunit --coverage-html reports và tải lại trang, ta sẽ thấy phần báo cáo đỏ đã chuyển thành xanh lá. Nếu di chuột lên đó, bạn sẽ thấy báo cáo mới có bài kiểm tra phần code này.

III. Chỉ số C.R.A.P – Change Risk Analysis and Predictions

Đây là giao diện khi click vào trang Dashboard.html của Coverage Reports.

unit testing tutorial iii 6

unit testing tutorial iii 7

 

Chỉ số C.R.A.P.(Change Risk Analysis and Predictions) là chỉ số phân tích rủi ro và dự đoán rủi ro của dự án. Hiểu đơn giản, nó cho bạn thấy những khó khăn và phương pháp cụ thể trong tương lai và tìm hiểu chính xác điều gì cần phải sửa đổi.

Tìm hiểu thêm về chỉ số C.R.A.P tại đây.

Có thể nói, phương thức mà bạn sử dụng có chỉ số C.R.A.P càng cao thì nó càng khó hiểu.

Nếu code của bạn đơn giản chỉ có một getter thì chỉ số C.R.A.P sẽ xấp xỉ bằng 1 (giá trị thấp nhất). Nếu code phức tạp thêm một chút, ví dụ như khối lệnh if, thì giá trị C.R.A.P của bạn sẽ bắt đầu tăng lên.

Còn nếu đoạn code có các vòng lặp như foreach lồng nhau, thì C.R.A.P sẽ tăng lên đáng kể.

Khi bạn viết các kiểm thử cho các tình huống thực thi khác nhau, chỉ số C.R.A.P của bạn bắt đầu giảm xuống. Khi tất cả các phương thức của bạn đều đã được xử lý thì chỉ số C.R.A.P cuối cùng của bạn sẽ nhỏ hơn rất nhiều so với khi chưa có kiểm thử nào.

Ở bài viết này, code của chúng ta không thực sự nhiều, không thực sự phức tạp nên chỉ số C.R.A.P rất thấp. Đây cũng là điều mà chúng ta nên hướng đến: các phương thức nhỏ, thực hiện các nhiệm vụ cụ thể. Điều này cho phép bất cứ Developer nào đọc cũng dễ hiểu và dễ dàng refactor code cũng như pass qua kiểm thử.

100% Code Coverage và tại sao không cần thiết?

Nhiều developer và các quản lý cho rằng cần viết kiểm thử cho đến khi bạn đã xử lý 100% code. 

Tuy nhiên, 100% không hẳn là cần thiết. Nếu phương thức của bạn có chỉ số C.R.A.P <5 thì nó không đủ phức tạp để cần phải kiểm thử.

Một phương thức có chỉ số C.R.A.P <5 có lẽ chỉ cần dưới 5 phút để viết kiểm thử cho nó. Vì thế, bạn có quyền quyết định xem mình sẽ muốn dành bao nhiêu thời gian và công sức để viết code kiểm thử.

IV. Kết

Hôm nay chúng ta đã tìm hiểu về làm thế nào để kiểm thử cho private/protected methods. Ta có thể thực hiện gián tiếp thông qua phương thức public, hoặc trực tiếp bằng cách sử dụng class ReflectionClass.

Ngoài ra, ta cũng biết được cách tạo Coverage Reports một cách hiệu quả, hay việc kiểm thử 100% thực sự không cần thiết.

Hy vọng những chia sẻ trong bài viết sẽ giúp ích cho bạn trên con đường chinh phục PHPUnit!


Hãy đọc thêm các phần còn lại của bài viết:

  1. UNIT TESTING TUTORIAL: LÀM QUEN VỚI PHPUNIT
  2. UNIT TESTING TUTORIAL: VIẾT MỘT KIỂM THỬ CÓ ÍCH VÀ @dataprovider

Trịnh Văn Thành – CO-WELL Asia