PHPUnit单元测试

一、概述

1. 什么是单元测试?
  • 【百度百科】单元测试是对软件中的最小可测单元进行检查和验证。
  • 是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。
2. 作用是什么?
  • 【废话】检查软件、程序的可行性,稳定性。
  • 通过单元测试能够避免在迭代、升级等过程中,引起重复的、多余的问题。
  • 避免在别人修改代码的时候,影响到你的逻辑
3. 哪些程序需要写单元测试(PHP)?
  • 【理想】理想的单元测试应当覆盖程序中所有可能的路径,包括正确的和错误的路径,个单元测试通常覆盖一个函数或方法中的一个特定路径。
  • 【现实】model、helper、controller中的函数必须测试、路径覆盖到所有可能性

二、PHPUnit的安装与集成CI框架

  • 略。。。。。后续再补

三、PHPUnit的使用

编写测试用例
  1. 测试的依赖关系 @depends

    PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。

    <?php
     class StackTest extends PHPUnit_Framework_TestCase
     {
         public function testEmpty()
         {
             $stack = array();
             $this->assertEmpty($stack);
     
             return $stack;
         }
     
         /**
    
    • @depends testEmpty

           */
          public function testPush(array $stack)
          {
              array_push($stack, 'foo');
              $this->assertEquals('foo', $stack[count($stack)-1]);
              $this->assertNotEmpty($stack);
      
              return $stack;
          }
      
          /**
      
      • @depends testPush

         */
        public function testPop(array $stack)
        {
            $this->assertEquals('foo', array_pop($stack));
            $this->assertEmpty($stack);
        }
        

        }
        ?>
        默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。如果需要传递对象的副本而非引用,则应当用 @depends clone 替代 @depends。

  2. 数据供给器 @dataProvider
    测试方法可以接受任意参数。用 @dataProvider 标注来指定使用哪个数据供给器方法。
    数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。

    <?php
    class DataTest extends PHPUnit_Framework_TestCase
    {
        /**
    
    • @dataProvider additionProvider

       */
      public function testAdd($a, $b, $expected)
      {
          $this->assertEquals($expected, $a + $b);
      }
      
      public function additionProvider()
      {
          return array(
            array(0, 0, 0),
            array(0, 1, 1),
            array(1, 0, 1),
            array(1, 1, 3)
          );
      }
      

      }
      ?>

    也可以是这样:

    <?php
    class DataTest extends PHPUnit_Framework_TestCase
    {
        /**
         * @dataProvider additionProvider
         */
        public function testAdd($a, $b, $expected)
        {
            $this->assertEquals($expected, $a + $b);
        }
    
        public function additionProvider()
        {
            return array(
              'adding zeros' => array(0, 0, 0),
              'zero plus one' => array(0, 1, 1),
              'one plus zero' => array(1, 0, 1),
              'one plus one' => array(1, 1, 3)
            );
        }
    }
    ?>
    
  3. 对PHP错误进行测试

    默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。利用这些异常,就可以,比如说,预期测试将触发 PHP 错误

    <?php
    class ExpectedErrorTest extends PHPUnit_Framework_TestCase
    {
        /**
    
    • @expectedException PHPUnit_Framework_Error

       */
      public function testFailingInclude()
      {
          include 'not_existing_file.php';
      }
      

      }
      ?>
      测试

      phpunit -d error_reporting=2 ExpectedErrorTest
      PHPUnit 5.2.0 by Sebastian Bergmann and contributors.

      .

      Time: 0 seconds, Memory: 5.25Mb

      OK (1 test, 1 assertion)

命令行测试执行器

说明

PHPUnit 测试执行器可通过phpunit 调用,例如在CI中:

tongkundeMacBook-Pro:www tongkun$ cd tests/
tongkundeMacBook-Pro:tests tongkun$ phpunit 
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.

.............                                                     13 / 13 (100%)

Time: 195 ms, Memory: 17.50Mb

OK (13 tests, 8 assertions)

说明:
先进入测试的根目录,执行phpunit 命令,后面可跟具体的目录或文件,也可不跟,如果没有则会对当前目录的所有文件执行单元测试,对于每个测试的运行,PHPUnit命令行工具会输出一个字符来指示进展:

  • . 当测试陈宫时输出
  • F 当测试方法运行过程中一个断言失败时输出,例如一个失败的assertEquals()调用
  • E 当测试方法运行过程中产生一个错误时输出,错误是指意料之外的异常(exception)或者PHP错误
  • R 当测试被标记有风险时输出
  • S 当测试跳出时输出
  • I 当测试被标记不完整或为实现时输出
常用命令行选项
  • --coverage-clover:为运行的测试生成带有代码覆盖率信息的 XML 格式的日志文件

  • --coverage-html:生成 HTML 格式的代码覆盖率报告

  • --coverage-php:生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息

  • --log-json:生成 JSON 格式的日志文件

  • --filter:只运行名称与给定模式匹配的测试。如果模式未闭合包裹于分隔符,PHPUnit 将用 / 分隔符对其进行闭合包裹

    tongkundeMacBook-Pro:tests tongkun$ phpunit --filter 'WelcomeTest::testTest'
    PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
    
    ...                                                                 3 / 3 (100%)
    
    Time: 148 ms, Memory: 16.75Mb
    
    OK (3 tests, 3 assertions)
    

    这样测试的就是WelcomeTest类中的testTest函数,过滤模式的例子有很多,详见文档官方

  • --colors:使用彩色输出。Windows下,用 ANSICON 或 ConEmu。

    本选项有三个可能的值:

    never: 完全不使用彩色输出。当未使用 --colors 选项时,这是默认值。

    auto: 如果当前终端不支持彩色、或者输出被管道输出至其他命令、或输出被重定向至文件时,不使用彩色输出,其余情况使用彩色。

    always: 总是使用彩色输出,即使当前终端不支持彩色、输出被管道输出至其他命令、或输出被重定向至文件。

    当使用了 --colors 选项但未指定任何值时,将选择 auto 做为其值。

  • --stop-on-error:首次错误出现后停止执行。

  • --stop-on-failure:首次错误或失败出现后停止执行。

  • --stop-on-risky:首次碰到有风险的测试时停止执行。

  • --stop-on-risky:首次碰到有风险的测试时停止执行。

  • --stop-on-incomplete首次碰到不完整的测试时停止执行。

  • --repeat:将测试重复运行指定次数。

    tongkundeMacBook-Pro:tests tongkun$ phpunit --repeat 10
    PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
    
    ...............................................................  63 / 130 ( 48%)
    ............................................................... 126 / 130 ( 96%)
    ....                                                            130 / 130 (100%)
    
    Time: 456 ms, Memory: 26.25Mb
    
    OK (130 tests, 80 assertions)
    
  • --tap:使用 Test Anything Protocol (TAP) 报告测试进度

    tongkundeMacBook-Pro:tests tongkun$ phpunit --tap
    TAP version 13
    ok 1 - CI_Unit_Test_class_Test::test_CI_Unit_Test_Class
    ok 2 - SomeControllerTest::testWelcomeController
    ok 3 - WelcomeTest::testIndex
    ok 4 - WelcomeTest::testTest
    ok 5 - WelcomeTest::testOutput
    ok 6 - WelcomeTest::testTest1
    ok 7 - WelcomeTest::testTest2
    ok 8 - HelperTest::testSampleFunction
    ok 9 - SomeLibTest::testMethod
    ok 10 - M_user_masterTest::testSelect
    ok 11 - M_user_masterTest::testInsert
    ok 12 - PHPTest::testFunctionJsonEncode
    ok 13 - PHPTest::testPhpVersion
    1..13
    
  • --configuration, -c:从 XML 文件中读取配置信息。更多细节请参见附录 C。

    如果 phpunit.xml 或 phpunit.xml.dist (按此顺序)存在于当前工作目录并且未使用 --configuration,将自动从此文件中读取配置。

    tongkundeMacBook-Pro:tests tongkun$ phpunit --configuration phpunit.xml 
    PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
    
    .............                                                     13 / 13 (100%)
    
    Time: 209 ms, Memory: 17.50Mb
    
    OK (13 tests, 8 assertions)
    
  • --no-configuration:忽略当前工作目录下的 phpunit.xml 与 phpunit.xml.dist。

四、基境(fixture)

什么是基境?

“基境”就是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。

基境的建立

PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp() 的模板方法。setUp() 是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown() 的模板方法。tearDown() 是清理测试所用对象的地方。

<?php
class StackTest extends PHPUnit_Framework_TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = array();
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
}
?>

测试类的每一个方法都会运行一次setUp()和tearDown()模板方法(同时,每个测试方法都在一个全新的测试类实例上运行),
另外,setUpBeforeClass() 与 tearDownAfterClass() 模板方法将分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用。基境共享可以在共享数据库连接时使用;

五、组织测试

用文件系统来编排测试套件

例如:

phpunit controllers/WelcomeTest.php 
用 XML 配置来编排测试套件
<?xml version="1.0" encoding="UTF-8"?>
<phpunit 
    //配置文件
    colors="true" //颜色
    stopOnFailure="false" //出错后是否终止 
    bootstrap="../application/third_party/CIUnit/bootstrap_phpunit.php"> //bootstrap 地址
    <php>
        <server name="SERVER_NAME" value="http://www.nyhdev.com" />
    </php>
    <testsuites>
        //测试套件
        <testsuite name="ControllerTests">
            <directory>controllers</directory> //要测试的目录
        </testsuite>
        <testsuite name="HelperTests">
            <directory suffix=".php">helpers</directory>
        </testsuite>
        <testsuite name="LibTests">
            <directory suffix=".php">libs</directory>
        </testsuite>
        <testsuite name="ModelTests">
            <directory suffix=".php">models</directory>
        </testsuite>
        <testsuite name="SystemTests">
            <directory suffix=".php">system</directory>
        </testsuite>
    </testsuites>
    <filter>
    <blacklist>
        <directory>vendor</directory>
        <directory>libs</directory>
    </blacklist>
    <whitelist>
        <directory>controllers</directory>
        <directory>fixtures</directory>
        <directory>models</directory>
        <directory>helpers</directory>
    </whitelist>
</filter>
</phpunit>

六、有风险的测试

无用测试

PHPUnit 可以更严格对待事实上不测试任何内容的测试。此项检查可以用命令行选项 --report-useless-tests 或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutTestsThatDoNotTestAnything="true" 来启用。

在启用本项检查后,如果某个测试未进行任何断言,它将被标记为有风险。仿件对象中的预期和诸如 @expectedException 这样的标注同样视为断言。

测试执行期间产生的输出

PHPUnit 可以更严格对待测试执行期间产生的输出。 此项检查可以用命令行选项 --disallow-test-output 或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutOutputDuringTests="true" 来启用。

在启用本项检查后,如果某个测试产生了输出,例如,在测试代码或被测代码中调用了 print,它将被标记为有风险。

七、未完成的测试与跳过的测试

未完成的测试

开始写新的测试用例类时,可能想从写下空测试方法开始,比如:

public function testSomething()
{
}

假如把成功的测试视为绿灯、测试失败视为红灯,那么还额外需要黄灯来将测试标记为未完成或尚未实现。PHPUnit_Framework_IncompleteTest 是一个标记接口,用于将测试方法抛出的异常标记为测试未完成或目前尚未实现而导致的结果。PHPUnit_Framework_IncompleteTestError 是这个接口的标准实现。

例如:我们有一个测试文件,contrllers/WelcomeTest.php,其中有一个测试方法,通过在测试方法中调用markTestIncomplete()将这个测试标记为未完成。

public function testTest() {
    $this->assertTrue(true,'这里可以正常工作');
    $this->markTestIncomplete('此测试尚未实现');
}

在PHPUnit命令行测试执行器中输出,未完成的测试标记为1, 如下:

localhost:tests tongkun$ phpunit 
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.

R..I...RRRR..                                                     13 / 13 (100%)

Time: 187 ms, Memory: 17.75Mb

OK, but incomplete, skipped, or risky tests!
Tests: 13, Assertions: 8, Incomplete: 1, Risky: 5.
跳过测试

如上,如果有些测试需要某些环境或者配置才能完成,则可选择跳过,通过调用 markTestSkipped() 方法来测试

用 @requires 来跳过测试

除了上述方法,还可以用 @requires 标注来表达测试用例的一些常见前提条件。
事例:

例 7.3: 用 @requires 来跳过测试
<?php
/**
 * @requires extension mysqli
 */
class DatabaseTest extends PHPUnit_Framework_TestCase
{
    /**
     * @requires PHP 5.3
     */
    public function testConnection()
    {
        // 测试要求有 mysqli 扩展,并且 PHP >= 5.3
    }

    // ... 所有其他要求有 mysqli 扩展的测试
}
?>

要求安装mysqli苦战和php 5.3 才能执行

#常用断言

前边废话一篇,终于到了关键的断言部分,断言可以说是单元测试的核心,通过断言的校验,保证程序的正确运行,并输出正确的值。

  • assertArrayHasKey()

    assertArrayHasKey(mixed $key, array $array[, string $message = ''])

    当 $array 不包含 $key 时报告错误,错误讯息由 $message 指定。

    assertArrayNotHasKey() 是与之相反的断言,接受相同的参数。

  • assertContains()

    assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])

    当 $needle 不是 $haystack的元素时报告错误,错误讯息由 $message 指定。

    assertNotContains() 是与之相反的断言,接受相同的参数。

    assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = FALSE])

    当 $needle 不是 $haystack 的子字符串时报告错误,错误讯息由 $message 指定。

  • assertContainsOnly()

    assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])

    当 $haystack 并非仅包含类型为 $type 的变量时报告错误,错误讯息由 $message 指定。

    $isNativeType 是一个标志,用来表明 $type 是否是原生 PHP 类型。

  • assertEmpty()

    assertEmpty(mixed $actual[, string $message = ''])

    当 $actual 非空时报告错误,错误讯息由 $message 指定。

    assertNotEmpty() 是与之相反的断言,接受相同的参数。

    assertAttributeEmpty() 和 assertAttributeNotEmpty() 是便捷包装(convenience wrapper),可以应用于某个类或对象的某个 public、protected 或 private 属性。

  • assertEquals()

    assertEquals(mixed $expected, mixed $actual[, string $message = ''])

    当两个变量 $expected 和 $actual 不相等时报告错误,错误讯息由 $message 指定。

    assertNotEquals() 是与之相反的断言,接受相同的参数。

    注意特定类型的比较(浮点型等),详见文档

  • assertFalse()

    assertFalse(bool $condition[, string $message = ''])

    当 $condition 为 TRUE 时报告错误,错误讯息由 $message 指定。

    assertNotFalse() 是与之相反的断言,接受相同的参数。

  • assertNull()

    assertNull(mixed $variable[, string $message = ''])

    当 $actual 不是 NULL 时报告错误,错误讯息由 $message 指定。

    assertNotNull() 是与之相反的断言,接受相同的参数。

  • assertRegExp()
    assertRegExp(string $pattern, string $string[, string $message = ''])

    当 $string 不匹配于正则表达式 $pattern 时报告错误,错误讯息由 $message 指定。

    assertNotRegExp() 是与之相反的断言,接受相同的参数。

  • assertStringMatchesFormat()

    assertStringMatchesFormat(string $format, string $string[, string $message = ''])

    当 $string 不匹配于 $format 定义的格式时报告错误,错误讯息由 $message 指定。

    assertStringNotMatchesFormat() 是与之相反的断言,接受相同的参数。

  • assertSame()
    assertSame(mixed $expected, mixed $actual[, string $message = ''])

    当两个变量 $expected 和 $actual 的值与类型不完全相同时报告错误,错误讯息由 $message 指定。

    assertNotSame() 是与之相反的断言,接受相同的参数。

    assertAttributeSame() 和 assertAttributeNotSame() 是便捷包装(convenience wrapper),以某个类或对象的某个 public、protected 或 private 属性作为实际值来进行比较。

  • assertTrue()

    assertTrue(bool $condition[, string $message = ''])

    当 $condition 为 FALSE 时报告错误,错误讯息由 $message 指定。

    assertNotTrue() 是与之相反的断言,接受相同的参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,800评论 6 13
  • PHP7 已经出来1年了,PHP7.1也即将和大家见面,这么多好的特性,好的方法,为什么不使用呢,也希望PHP越来...
    梦幻_78af阅读 2,062评论 1 10
  • ES6(ECMAScript2015)的出现,无疑给前端开发人员带来了新的惊喜,它包含了一些很棒的新特性,可以更加...
    c盖世阅读 153评论 0 1
  • 经常会听到这样一句话。学历代表过去,能力代表现在,学习力代表未来。就是说学习力大于学历。就是说要想让自己有好的未来...
    胜利一号阅读 473评论 0 0