圣泉山人 后端开发工程师

PHP单元测试(一) 基础

2018-05-02

概述

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。 –百度百科

安装phpunit

###

composer require --dev phpunit/phpunit ^6.2

测试demo

class Transfer {
    private $accountA = 100;
    private $accountB = 100;

    public function aToB(int $money)
    {
        $this->accountA -= $money;
        $this->accountB += $money;
    }

    public function bToA(int $money)
    {
        $this->accountB -= $money;
        $this->accountA += $money;
    }

    public function getAccountA()
    {
        return $this->accountA;
    }

    public function getAccountB()
    {
        return $this->accountB;
    }
}

class TestTransfer extends  TestCase{
    private $transferObj;

    public function setUp()
    {
        $this->transferObj = new Transfer();
    }
    public function testAtoB()
    {
        $originalA = $this->transferObj->getAccountA();
        $originalB = $this->transferObj->getAccountB();

        $this->transferObj->aToB(10);
        $this->assertEquals($originalA - 10, $this->transferObj->getAccountA());
        $this->assertEquals($originalB + 10, $this->transferObj->getAccountB());
    }
}

执行测试:

./vendor/bin/phpunit index.php 
PHPUnit 7.5.1 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 543 ms, Memory: 4.00MB

OK (1 test, 2 assertions)

或者使用 phpunit.phar

wget http://phar.phpunit.cn/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version
phpunit index.php

详细使用

基境

PHPUnit 支持共享建立基境的代码。

提供了以下几个模板方法:

  • setUpBeforeClass : 测试用例类的第一个测试运行之前执行
  • tearDownAfterClass : 测试用例类的最后一个测试运行之后执行
  • setUp : 每个测试方法运行之前执行
  • tearDown : 每个测试方法运行之后执行

==注意:每个测试方法都是在一个全新的测试类实例上运行的==

全局状态

  • 全局变量:有时候测试代码中用到了全局变量($_GLOBALS),但是如果对这里面的变量进行了修改,可能会导致其他测试方法出现问题,那么怎么保证每个测试方法都使用的是一样的全局变量呢? 通过:@backupGlobals disabled|enabled 它可标注在:
    • 测试类 : 作用范围为整个测试类
    • 测试方法 : 作用范围为这个方法
      /**
       * @backupGlobals disabled
       */
      class MyTest extends TestCase
      {
          // ...
      }
        
      /**
       * @backupGlobals disabled
       */
      class MyTest extends TestCase
      {
          /**
           * @backupGlobals enabled
           */
          public function testThatInteractsWithGlobalVariables()
          {
              // ...
          }
      }
    

    支持设置 “全局变量黑名单” 黑名单中的全局变量将被排除于备份与还原操作之外:

      class MyTest extends TestCase
      {
          protected $backupGlobalsBlacklist = ['globalVariable'];
          // ...
      }
    

    对于全局变量的备份和还原的原理是使用了:serialize()unserialize()

    注意:

    • 对于无法被序列化的对象放入 $GLOBALS 数组内时,备份操作就会出问题。比如:PDO
    • 在方法(例如 setUp())内对 $backupGlobalsBlacklist 属性进行设置是无效的
  • 类的静态属性 。对于类的静态属性的备份和还原可以通过:@backupStaticAttributes enabled|disabled

    作用对象:在测试开始时已声明的所有类(而不仅是测试类自身),且只作用于静态类属性,不作用于函数内声明的静态变量。

    使用位置和 backupGlobals 一致:

    • 测试类
    • 测试方法

    只有启用了 @backupStaticAttributes 的测试方法才会在方法之前执行此操作。如果在此之前运行的某个没有启用 @backupStaticAttributes 的测试方法改变了静态属性的值,那么被备份及还原的将会是这个改变后的值

    同样提供了黑名单支持:

      class MyTest extends TestCase
      {
          protected $backupStaticAttributesBlacklist = [
              'className' => ['attributeName']
          ];
        
          // ...
      }
    

依赖关系

使用 @depends 声明测试方法所依赖的其他测试方法。 依赖方法的返回值,会作为被依赖方法的参数,其顺序和 @depends 的顺序一致,但是不会影响代码的执行顺序。

public function testOne()
{
    $this->assertTrue(true);
    return "depends1";
}

public function testTwo()
{
    $this->assertTrue(true);
    return "depends2";
}

/**
 * 参数顺序对应 @depends 的顺序
 * @depends testOne
 * @depends testTwo
 */
public function testDepends()
{
    $this->assertEquals(['depends1', 'depends2'], func_get_args());
}

测试结果:

...                                                                 3 / 3 (100%)

Time: 573 ms, Memory: 4.00MB

OK (3 tests, 3 assertions)

注意:

  • 当被依赖的测试方法失败时,不会再执行依赖方法的测试。
  • 如果被依赖方法返回的是对象,默认是引用传递,如果希望传递对象的副本时,使用: @depends clone

数据供给器

使用 @dataProvider 声明数据供给器。 对应的方法需要返回:

  • 数组(每个元素也是数组)
  • 可遍历的对象(实现了迭代接口)

然后测试时,会将每次迭代器提供的一组数据进行测试,直到全部遍历完毕。

示例:

/**
 * @dataProvider addtionProvider
 */
public function testSum($a, $b, $res)
{
    $this->assertEquals($res, $a + $b);
}

public function addtionProvider()
{
    return [
        [1,3,4],
        [1,1,2],
        [1,1,3],
    ];
}

测试结果:

PHPUnit 7.5.1 by Sebastian Bergmann and contributors.

..F                                                                 3 / 3 (100%)

Time: 622 ms, Memory: 4.00MB

There was 1 failure:

1) TestDepends::testSum with data set #2 (1, 1, 3)
Failed asserting that 2 matches expected 3.

/vagrant/www/myyphp/index.php:16

FAILURES!
Tests: 3, Assertions: 3, Failures: 1.

注意点:

  • 和 @depends 同时使用时,@provider 提供的参数会优先于 @depends 提供的参数,并且,依赖关系提供的参数不会变化。

      public function testOne()
      {
          $this->assertTrue(true);
          return 'Depends1';
      }
        
      /**
       * @depends testOne
       * @dataProvider addtionProvider
       */
      public function testSum()
      {
          $this->assertEquals(['Provider1','Depends1'], func_get_args());
      }
        
      //会测试两次,第一此传递:Provider1,第二次传递:Provider2
      public function addtionProvider()
      {
          return [
              ['Provider1'],
              ['Provider2'],
          ];
      }
    

    结果:

      PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
        
      ..F                                                                 3 / 3 (100%)
        
      Time: 644 ms, Memory: 4.00MB
        
      There was 1 failure:
        
      1) TestDepends::testSum with data set #1 ('Provider2')
      Failed asserting that two arrays are equal.
      --- Expected
      +++ Actual
      @@ @@
       Array (
      -    0 => 'Provider1'
      +    0 => 'Provider2'
           1 => 'Depends1'
       )
        
      /vagrant/www/myyphp/index.php:23
        
      FAILURES!
      Tests: 3, Assertions: 3, Failures: 1.
    
  • 如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行 ```php /**
    • @dataProvider addtionProvider */ public function testSum($a, $b, $res) { $this->assertEquals($res, $a + $b); return “depends”; }

    /**

    • 由于依赖的测试方法,全部都未通过测试,此时不会执行这个测试
    • @depends testSum */ public function testOne() { $this->assertTrue(true); }

    public function addtionProvider() { return [ [1,3,0], [1,1,0], ]; }

      此时,数据供给器提供的数据全部无法通过测试,此时 `testOne` 测试方法不会被执行,会跳过:
      ```php
      PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
    
      FFS                                                                 3 / 3 (100%)
        
      Time: 654 ms, Memory: 4.00MB
        
      There were 2 failures:
        
      1) TestDepends::testSum with data set #0 (1, 3, 0)
      Failed asserting that 4 matches expected 0.
        
      /vagrant/www/myyphp/index.php:16
        
      2) TestDepends::testSum with data set #1 (1, 1, 0)
      Failed asserting that 2 matches expected 0.
        
      /vagrant/www/myyphp/index.php:16
        
      FAILURES!
      Tests: 3, Assertions: 2, Failures: 2, Skipped: 1.
    
    
  • 使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。也就是说,不能通过 return 返回值传到依赖于它的方法。 ```php /**
    • @dataProvider addtionProvider */ public function testSum($a, $b, $res) { $this->assertEquals($res, $a + $b); return ‘hi’; }

    /**

    • @depends testSum */ public function testOne() { $this->assertEquals([‘hi’], func_get_args()); }

    public function addtionProvider() { return [ [1,3,4], ]; }

      无法接收到参数:
      ```php
      .F                                                                  2 / 2 (100%)
    
      Time: 616 ms, Memory: 4.00MB
        
      There was 1 failure:
        
      1) TestDepends::testOne
      Failed asserting that two arrays are equal.
      --- Expected
      +++ Actual
      @@ @@
      Array (
      -    0 => 'hi'
      +    0 => null
      )
        
      /vagrant/www/myyphp/index.php:26
        
      FAILURES!
      Tests: 2, Assertions: 2, Failures: 1.
    
  • 所有的数据供给器方法的执行都是在对 setUpBeforeClass 静态方法的调用和第一次对 setUp 方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。

异常测试

异常测试有两种方式:

  • 在代码中使用: $this->expectException(InvalidArgumentException::class);
  • 使用标注:@expectException

断言方法/标注:

  • expectException
  • expectExceptionCode
  • expectExceptionMessage
  • expectExceptionMessageRegExp
public function testException1()
{
    $this->expectException(InvalidArgumentException::class);
    $this->expectExceptionMessage("hello");
    throw new Exception('hello');
}

/**
 *  @expectedException InvalidArgumentException
 *  @expectedExceptionMessage hi
 */
public function testException2()
{
    throw new InvalidArgumentException('hello');
}

运行结果:

FF                                                                  2 / 2 (100%)

Time: 171 ms, Memory: 10.00MB

There were 2 failures:

1) TestDemo::testException1
Failed asserting that exception of type "Exception" matches expected exception "InvalidArgumentException". Message was: "hello" at
/vagrant/www/myyphp/index.php:15
.

2) TestDemo::testException2
Failed asserting that exception message 'hello' contains 'hi'.

FAILURES!
Tests: 2, Assertions: 3, Failures: 2.

注意:不允许对 :Exception 类进行测试,异常类越明确越好。

错误测试

默认情况下,在测试过程中如果触发到了 PHP 的错误/警告,PHPUnit 会将其转换为异常:

  • PHPUnit\Framework\Error\Error\Notice
  • PHPUnit\Framework\Error\Error\Warning
  • PHPUnit\Framework\Error\Error\Error
public function testError()
{
    $this->expectException(PHPUnit\Framework\Error\Error::class);
    //此时触发一个错误
    include 'file_not_existing_file.php';
}

结果:

.                                                                   1 / 1 (100%)

Time: 163 ms, Memory: 10.00MB

OK (1 test, 1 assertion)

注意:如果测试依靠会触发错误的 PHP 函数,例如 fopen ,我们可以通过抑制住错误通知,就能对返回值进行检查,否则错误通知将会导致抛出异常:

use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

输出内容测试

有时候,想要断言 某方法的运行过程中生成了预期的输出(例如,通过 echo 或 print)。PHPUnit\Framework\TestCase 类使用 PHP 的 输出缓冲 特性来为此提供必要的功能支持。

public function testOutput1()
{
    $this->expectOutputString("hello");
    echo "hello";
}
public function testOutput2()
{
    //正则匹配
    $this->expectOutputRegex("/\d+/");
    print "hello world";
}

运行结果:

.F                                                                  2 / 2 (100%)

Time: 169 ms, Memory: 10.00MB

There was 1 failure:

1) TestDemo::testOutput2
Failed asserting that 'hello world' matches PCRE pattern "/\d+/".

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

用于输出内容测试的方法:

方法 含义
void expectOutputRegex(string $regularExpression) 设置输出预期为输出应当匹配正则表达式。
void expectOutputString(string $expectedString) 设置输出预期为输出应当与 $expectedString 字符串相等。
bool setOutputCallback(callable $callback) 设置回调函数,用来做诸如将实际输出规范化之类的动作。
string getActualOutput() 获取实际输出。

==注意:在严格模式下,本身产生输出的测试将会失败。==

标记未完成 与 跳过

  • 标记未完成

      public function testMark() {
          $this->assertTrue(true);
          //在这里停止
          $this->markTestIncomplete('后续还未完成');
      }
    

    执行结果:

      I            1 / 1 (100%)
        
      Time: 253 ms, Memory: 4.00MB
        
      OK, but incomplete, skipped, or risky tests!
      Tests: 1, Assertions: 1, Incomplete: 1.
    
  • 跳过测试

      public function setUp() {
          if (!extension_loaded('mysqli')) {
              $this->markTestSkipped('MySQLi 扩展不可用');
          }
      }
      public function testMark() {
          $this->assertTrue(true);
      }
    

    测试结果:

      .               1 / 1 (100%)
    
      Time: 239 ms, Memory: 4.00MB
        
      OK (1 test, 1 assertion)
    

    还可以使用 @requires 来标识条件,如果条件不满足,也会跳过:

      /**
       * @requires PHP 7.3.0
       */
      public function testMark() {
          $this->assertTrue(true);
      }
    

    其他条件: 类型| 可能的值| 范例| 其他范例 —|—|—|—|— PHP| 任何 PHP 版本标识符 |@requires PHP 5.3.3| @requires PHP 7.1-dev PHPUnit| 任何 PHPUnit 版本标识符 |@requires PHPUnit 3.6.3| @requires PHPUnit 4.6 OS| 用来对 PHP_OS 进行匹配的正则表达式| @requires OS Linux| @requires OS WIN32|WINNT function| 任何对 function_exists 而言有效的参数 |@requires function imap_open| @requires function ReflectionMethod::setAccessible extension| 任何扩展模块名,可以附带有版本标识符 |@requires extension mysqli | @requires extension redis 2.2.0

组织测试

PHPUnit 支持多种将多个测试组织在一起,形成组合测试套件。

  • 文件系统方式
    • 缺点:无法控制测试执行的顺序
  • 用 XML 配置方式

文件系统方式

把所有测试用例源文件放在一个测试目录中,PHPUnit 会自动发现并运行测试 *Test.php 文件。

比如:

-vendor
-testes
    Demo1Test.php
    Demo1Test.php
    Demo1Test.php

执行:

 ./vendor/bin/phpunit ./testes
 
 //或者:
 ./vendor/bin/phpunit --bootstrap ./vendor/autoload.php ./testes/

即会自动执行这个3个测试类文件中的测试方法。

如果想要对运行哪些测试有更细粒度的控制,可以使用 --filter 选项,示例:

 /vendor/bin/phpunit --filter Demo1Test ./testes

### XML 配置方式

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
         backupGlobals="true"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
>
<!-- 测试套件标签 -->
    <testsuites>
        <!-- 单个测试套件 -->
        <testsuite name="demo">
            <!-- 指定测试目录下的全部文件 -->
            <directory>testes</directory>
            <!-- 指定测试文件 -->
            <file>test1/Demo4Test.php</file>
            <!-- php的版本大于等于5.3.0才将文件添加到套件中 -->
            <file phpVersion="5.3.0" phpVersionOperator=">=">test1/Demo5Test.php</file>
        </testsuite>
    </testsuites>
</phpunit>

根标签中更多的配置项参见手册:http://www.phpunit.cn/manual/current/zh_cn/appendixes.configuration.html

中文手册:http://www.phpunit.cn/manual/6.5/zh_cn/index.html


Similar Posts

下一篇 AES算法

Comments