单位内单测沙龙

#单元测试 #PHPUnit #沙龙

头脑风暴

什么样的代码算一个单元?

单测里一个方法或函数是一个最小单元。

黑盒还是白盒?
  • 黑盒:让一个不明白代码逻辑的人来测试。
  • 白盒:让一个理解代码逻辑的人来测试。
单元测试对一个项目来说非常重要?
  • “保证最小单元是正确的”,大模块出问题的可能性就会很小。
  • 保证修改的代码没有对以前的特性造成破坏,能够快速验证。
  • 提升自信心
  • 提高代码质量
国内现状
  • 高度迭代,测试占时不宜过高。快糙猛是PHP的标签之一。
  • 项目管理质量太差,导致项目乱,迭代不稳定。
  • 代码质量低,出现大量的加班,改需求,改代码。
  • 上述场景下写测试会比较浪费时间。
正确理解单元测试
  • 单测的本质是正确的隔离(isolate)待测代码。
  • 不要发大量时间在前置条件上。
  • 伪造。 ~~ Notes 很多同事埋冤甚至咒骂写单元测试这件事情,其实症结就在浪费太多精力在创造完成断言的前置条件上,就差这一层窗户纸,只要能理解“隔离”这两个字在单元测试中的意义就能捅破它。 ~~
测试驱动开发
  • 准备编写自己觉得没谱的代码,小步快速走。
  • 重构代码。
行为驱动开发
  • 可以说是为了重构而生,所以它随着敏捷化方法论而开始被人熟知。
  • 测试先于代码实现并指导代码实现的最终结果。
避免无用的测试
  1. 是否一定要遵循TDD原则?不一定。
  2. 不要去测试语言的核心库和/或标准库函数
  3. 不要去测试框架的基础类或工具方法
  4. 不要去测试外部依赖的有效性
  5. 只见绿灯,不见红灯
代码测试覆盖率

简单普及下,代码覆盖算法有很多种,大致上对比准确性:路径覆盖 > 条件覆盖 ~= 判定覆盖 > 语句覆盖。
一种辅助度量工具,不可作为核心指标(容易走偏)。

开启单测之旅

安装
  • 命令

wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit –version
// PHPUnit x.x.x by Sebastian Bergmann and contributors.

  • composer
{
 "require-dev": {
 "phpunit/phpunit": "5.0.*"
 }
}
$ composer install
// or
$ composer global require "phpunit/phpunit=5.0.*"
用例规范
  • 文件名:*Test.php。
  • 类名和文件名一致。
  • 继承PHPUnit_Framework_TestCase
  • 方法名带test前缀。
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
 public function testOne()
 {
  $this->assertTrue(true);
  $this->assertFalse(false);
 }
}
$ phpunit FooTest.php
PHPUnit 4.8.5 by Sebastian Bergmann and contributors.
.
Time: 150 ms, Memory: 13.25Mb
OK (1 test, 2 assertions)
测试依赖关系

@depends 标注来表达依赖关系 默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。多重依赖依次传递给相应的消费者。
当某个测试所依赖的测试失败时,PHPUnit 会跳过这个测试。

数据供给器

@dataProvider 标注来指定使用哪个数据供给器方法。

  public function additionProvider()
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
  1. 必须为public
  2. 返回值为数组或实现了Iterator接口的对象
  3. 最好是键值数组,输出的信息会更加详细
  4. 一个测试中@dataProvider与@depends同时使用时,@dataProvider优先级要高于@depends
  5. 无法在数据供给器中使用创建于setUp()和setUpBeforeClass内的变量。
对异常进行测试
  • 使用的标记 @expectedException @expectedExceptionMessage @expectedExceptionMessageRegExp @expectedExceptionCode
  • 使用的内置标记
$this->setExpectedException()
$this->setExpectedExceptionRegExp()
如何组织用例
  • 通过文件结构
  • 通过XML配置文件, phpunit.xml.dist
    可以明确指定测试的执行顺序:
<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <file>tests/IntlFormatterTest.php</file>
      <file>tests/MoneyTest.php</file>
      <file>tests/CurrencyTest.php</file>
    </testsuite>
  </testsuites>
</phpunit>
测试结果标识符
  • . 当测试成功时输出。
  • F 当测试方法运行过程中一个断言失败时输出。
  • E 当测试方法运行过程中产生一个错误时输出。
  • R 当测试被标记为有风险时输出。
  • S 当测试被跳过时输出。
  • I 当测试被标记为不完整或未实现时输出。
未完成的测试与跳过的测试
//未完成测试
void markTestIncomplete()	将当前测试标记为未完成。
void markTestIncomplete(string $message)
//跳过测试
void markTestSkipped()	将当前测试标记为已跳过。
void markTestSkipped(string $message)

用 @requires 标注来表达测试用例的一些常见前提条件,若不满足则跳过测试。

类型 范例
PHP @requires PHP 5.3.3
OS @requires OS Linux
function @requires function imap_open
extension @requires extension mysqli
PHPUnit @requires PHPUnit 3.6.3

基镜(fixture)

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

模板方法

  • setUpBeforeClass()
  • setUp()
  • assertPreConditions()
  • assertPostConditions()
  • tearDown()
  • onNotSuccessfulTest()
  • tearDownAfterClass()
测试替身–隔离依赖
  • 使用PHPUnit内置的MockBuilder
  • 使用第三方工具Mockery(推荐使用)

    将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。可以用桩件(stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。

    将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)。

final、private 和 static 方法无法对其进行上桩(stub)或模仿(mock)。

Stubs(桩件)示例

对SomeClass类进行上桩,返回固定值。

<?php
class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        // 为 SomeClass 类创建桩件。
        $stub = $this->createMock(SomeClass::class);

        // 配置桩件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 现在调用 $stub->doSomething() 将返回 'foo'。
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>

willReturn($value) = will($this->returnValue($value))
returnArgument()
returnSelf()
returnValueMap()
returnCallback()
throwException()
仿件对象(Mock Object)示例

模仿Something调用一次,返回固定值

<?php
class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        $stub = $this->getMockBuilder(SomeClass::class);
        $stub->setMethods(['doSomething'])
             ->getMock();
        $stub->expects($this->once())
             ->method('doSomething')
             ->with($this->equalTo('something'));
        
       $st = new SomeClass();
       $st->soSomething('something');
    }
}
?>

with() 方法可以携带任何数量的参数
withConsecutive() 方法可以接受任意多个数组作为参数
callback() 约束用来进行更加复杂的参数校验
匹配器:any(),never(),atLeastOnce(),once(),exactly(int $count),at(int $index)

DB测试
  • 建立基境(fixture)
  • 执行被测系统
  • 验证结果
  • 拆除基境(fixture)

Thanks for looking :-)

https://phpunit.de/manual/current/zh_cn