iT邦幫忙

DAY 14
5

逐步提昇PHP技術能力系列 第 14

逐步提昇PHP技術能力 - 開發工具 : 使用PHPUnit進行單元測試

  • 分享至 

  • xImage
  •  

安裝完畢PHPUnit之後,最重要的當然還是拿來做單元測試,所以還是用幾個實際的case來嘗試一下,看看做單元測試的基本方法。

另外,測試是否可以涵蓋測試對象的所有邏輯也是需要考慮的問題。透過xdebug的支援,PHPUnit可以在測試時同時記錄覆蓋率,這樣可以比較清楚知道目前寫的測試是否足夠。

昨天的測試中,一次只跑了一個test case,如果需要測試的東西多,一支一支跑會很麻煩,所以PHPUnit可以把test case組織成test suite,批次進行測試。

今天就把這單元測試、測試結果輸出、測試覆蓋率、組織test suite等功能都試一試。
參考:http://phpunit.de/manual/3.7/en/index.html

* 撰寫單元測試

要能進行單元測試,起碼的要求是程式中有「單元」,這是程式執行的最小單位,也就是函數或是類別的方法。(PHP是可以寫成完全沒單元...這樣就沒辦法做單元測試)

測試的方法很簡單,就是把參數丟給函數或方法,然後檢測它執行的結果是否符合預期。先寫一小段程式,然後使用phpunit來測試:

<?php
class ScormTimeUtils
{
    public static function SCORMTimeParser($time)
    {
        $ret = 0;
        $comp = explode(':', trim($time));
        if (count($comp)!==3) return 0;
        $hour = intval($comp[0]);
        if ($hour<0) {
            $hour = abs($hour);
            $sign = -1;
        } else {
            $sign = 1;
        }
        $min = intval($comp[1]);
        $sec = floatval($comp[2]);
        $ret = ($sec + $min*60 + $hour*60*60)*1000;
        return $ret*$sign;
    }
    public static function SCORMTimeFormat($milisec)
    {
        if (intval($milisec)<0) {
            $milisec = abs($milisec);
            $sign = '-';
        } else {
            $sign = '';
        }
        $pri = intval($milisec)/1000;
        $rest = floor($pri);
        $mili = round(floatval($pri - $rest),3)*1000;
        $sec = $rest%60;
        $rest1 = floor($rest/60);
        $min = $rest1%60;
        $hour = floor($rest1/60);
        $ret = sprintf("$sign%04d:%02d:%02d.%03d", $hour, $min, $sec, $mili);
        return $ret;
    }
    public static function SCORMTimeAdd($time1, $time2)
    {
        return self::SCORMTimeFormat(self::SCORMTimeParser($time1)+self::SCORMTimeParser($time2));
    }
    public static function SCORMTimeDiff($time1, $time2)
    {
        return self::SCORMTimeFormat(abs(self::SCORMTimeParser($time1)-self::SCORMTimeParser($time2)));
    }
}

這是一個計算特殊時間格式的工具,可以用SCORMTimeParser把時間轉成毫秒,或是用SCORMTimeFormat把毫秒轉成這個時間格式。組合這兩個,就可以做時間的加減。然後寫一個簡單的測試:

<?php
require "vendor/autoload.php";
require "src/ScormTimeUtils.php";

class TestScormTimeUtils extends PHPUnit_Framework_TestCase
{
    public $fixture1 = array("0000:20:21.023", "0002:23:20.324", "0002:43:41.347");
    public $fixture2 = array('0000:00:23.043', 23043);
    public function testScormTimeAdd() {
        $this->assertEquals(ScormTimeUtils::SCORMTimeAdd($this->fixture1[0], $this->fixture1[1]), $this->fixture1[2]);
        $this->assertEquals(ScormTimeUtils::SCORMTimeAdd('0000:00:00.000', '0000:00:00.000'), '0000:00:00.000');
    }
    public function testScormTimeDiff() {
        $this->assertEquals(ScormTimeUtils::SCORMTimeDiff($this->fixture1[2], $this->fixture1[1]), $this->fixture1[0]);
    }
    public function testScormTimeParser() {
        $this->assertEquals(ScormTimeUtils::SCORMTimeParser($this->fixture2[0]), $this->fixture2[1]);
    }
    /**
    * @expectedException InvalidArgumentException
    * @test
    */
    public function doScormTimeParserException() {
        ScormTimeUtils::SCORMTimeParser('0000:00:61.000');
    }
    public function testScormTimeFormat() {
        $this->assertEquals(ScormTimeUtils::SCORMTimeFormat($this->fixture2[1]), $this->fixture2[0]);
    }
}

除了使用test開頭的方法名稱,phpunit也支援annotation,例如使用@test可以讓不是test開頭的方法,也要加入測試。另外,加上expectedException,phpunit就知道至個測試應該會觸發某個指定的Exception。
測試結果:

PHPUnit 3.7.27 by Sebastian Bergmann.

Configuration read from /Users/fillano/builds/ironman6/2-3a/phpunit.xml

...F.

Time: 168 ms, Memory: 3.25Mb

There was 1 failure:

1) TestScormTimeUtils::doScormTimeParserException
Failed asserting that exception of type "InvalidArgumentsException" is thrown.

/Users/fillano/.composer/vendor/phpunit/phpunit/PHPUnit/Framework/TestSuite.php:775
/Users/fillano/.composer/vendor/phpunit/phpunit/PHPUnit/Framework/TestSuite.php:745
/Users/fillano/.composer/vendor/phpunit/phpunit/PHPUnit/Framework/TestSuite.php:705
/Users/fillano/.composer/vendor/phpunit/phpunit/PHPUnit/TextUI/Command.php:176
/Users/fillano/.composer/vendor/phpunit/phpunit/PHPUnit/TextUI/Command.php:129

FAILURES!
Tests: 5, Assertions: 6, Failures: 1.

結果測試沒通過,仔細看一下,會發現傳了不正確的參數,卻沒有觸發該有的Exception。所以回頭改一下程式:

    public static function SCORMTimeParser($time)
    {
        $ret = 0;
        $comp = explode(':', trim($time));
        if (count($comp)!==3) return 0;
        $hour = intval($comp[0]);
        if ($hour<0) {
            $hour = abs($hour);
            $sign = -1;
        } else {
            $sign = 1;
        }
        $min = intval($comp[1]);
        if ($min>=60) {
        	throw new InvalidArgumentException("Minutes over 60.");
        }
        $sec = floatval($comp[2]);
        if ($sec >= 60.0) {
        	throw new InvalidArgumentException("Seconds over 60.");
        }
        $ret = ($sec + $min*60 + $hour*60*60)*1000;
        return $ret*$sign;
    }

改過以後再測試一下:

PHPUnit 3.7.27 by Sebastian Bergmann.

Configuration read from /Users/fillano/builds/ironman6/2-3a/phpunit.xml

.....

Time: 128 ms, Memory: 3.25Mb

OK (5 tests, 6 assertions)

目前程式跟測試的結果,看起來就相符了。

* 用不同的方式輸出測試結果

除了直接輸出到console,phpunit也支援多種輸出格式,例如與JUnit相同格式的xml檔,或是html檔等等。

JUnit是Java環境中最主要的單元測試框架,所以支援很完整。輸出成JUnit格式,就可以讓許多實際上並不直接支援phpunit的系統也可以吃phpunit的測試結果。

例如上面的測試,可以使用phpunit --log-junit reports/utils.xml輸出,結果看起來就像這樣:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="utils" tests="5" assertions="6" failures="0" errors="0" time="0.017013">
    <testsuite name="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" tests="5" assertions="6" failures="0" errors="0" time="0.017013">
      <testcase name="testScormTimeAdd" class="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" line="9" assertions="2" time="0.009504"/>
      <testcase name="testScormTimeDiff" class="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" line="13" assertions="1" time="0.001663"/>
      <testcase name="testScormTimeParser" class="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" line="16" assertions="1" time="0.001643"/>
      <testcase name="doScormTimeParserException" class="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" line="23" assertions="1" time="0.002778"/>
      <testcase name="testScormTimeFormat" class="TestScormTimeUtils" file="/Users/fillano/builds/ironman6/2-3a/tests/TestScormTimeUtils.php" line="26" assertions="1" time="0.001425"/>
    </testsuite>
  </testsuite>
</testsuites>

* 檢視測試覆蓋率

測試覆蓋率對於測試是否足夠,是一個參考的指標。只要有安裝xdebug這個zend extention,phpunit就可以利用他統計出測試所跑過的程式碼與所有程式碼的比率。透過這個數字,可以大致評估是否程式執行的所有路徑,測試都有跑到。

要檢視測試覆蓋率,加上參數就可以。例如用phpunit --coverage-text=reposts/utils.txt來執行測試,覆蓋率的報告就會存入reports/utils.txt檔案中:

Code Coverage Report 
  2013-10-14 22:39:18

 Summary: 
  Classes: 0.00% (0/13)
  Methods: 1.89% (2/106)
  Lines:   1.41% (32/2270)

ScormTimeUtils
  Methods: 100.00% ( 4/ 4)   Lines:  78.79% ( 26/ 33)
\Composer\Autoload::ClassLoader
  Methods:  16.67% ( 2/12)   Lines:   7.59% (  6/ 79)

看起來他把包含phpunit的程式都一起估計了,比較有用的是關於ScormTimeUtils的估計,可以看到總共33行程式,測試有跑到的程式有26行,覆蓋率是78.79%。覺得還不夠好,所以回頭看了一下測試。ScormTimeUtils::SCORMTimeParser有兩個地方會拋出Exception,但是我只測了一個。所以再加一個測試,同時把原來的測試名稱改一下,方便辨識:

    /**
    * @expectedException InvalidArgumentException
    * @test
    */
    public function doScormTimeParserSecException() {
        ScormTimeUtils::SCORMTimeParser('0000:00:61.000');
    }
    /**
    * @expectedException InvalidArgumentException
    * @test
    */
    public function doScormTimeParserMinException() {
        ScormTimeUtils::SCORMTimeParser('0000:62:59.000');
    }

再跑一次測試覆蓋率,這次就超過80%了(只多跑了一行XD):

Code Coverage Report 
  2013-10-14 22:49:00

 Summary: 
  Classes: 0.00% (0/13)
  Methods: 1.89% (2/106)
  Lines:   1.45% (33/2270)

ScormTimeUtils
  Methods: 100.00% ( 4/ 4)   Lines:  81.82% ( 27/ 33)
\Composer\Autoload::ClassLoader
  Methods:  16.67% ( 2/12)   Lines:   7.59% (  6/ 79)

一般來說,測試覆蓋率最好都要超過80%,測試才比較完整。改成phpunit --coverage-html coverage,會輸出html到coverage目錄,這樣可以看到更詳細的結果:

背景黃色的是有跑到的程式,紅色的是沒跑到的。要跑到沒有跑到的部份,就需要加更多測試來做。(這裡其實給SCORMTimeParser的小時加個負號,或是給SCORMTimeFormat的參數改成負數,就可以做到了)

* 組織test suite

有很多測試檔時,可以透過test suite來組織測試。phpunit可以直接指定目錄,然後phpunit會將目錄內所有*test.php檔案視為測試檔來跑測試。另外一個方法是利用一個test suite xml檔來指定。我在上例中其實已經使用了,大概是長這樣:

<phpunit>
  <testsuites>
    <testsuite name="utils">
      <file>tests/TestScormTimeUtils.php</file>
    </testsuite>
  </testsuites>
</phpunit>

結構很簡單,<file>也可以換成目錄<directory>。檔案中可以包含多個<testsuite>,然後透過--testsuite來指定要跑哪個測試組。

========

其實要深入的話,光單元測試就可以作為一個鐵人賽題目了XD,今天先在這裡打住吧。


上一篇
逐步提昇PHP技術能力 - 開發工具 : PHPUnit
下一篇
逐步提昇PHP技術能力 - 開發工具 : 使用Selenium進行整合測試
系列文
逐步提昇PHP技術能力30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
老鷹(eagle)
iT邦高手 1 級 ‧ 2013-10-14 23:38:09

fillano提到:
先寫一小段程式

很大一段暈毆飛

0

我要留言

立即登入留言