使用PHPUnit进行单元测试并生成代码覆盖率报告的方法


Posted in PHP onMarch 08, 2019

安装PHPUnit

使用 Composer 安装 PHPUnit

#查看composer的全局bin目录 将其加入系统 path 路径 方便后续直接运行安装的命令
composer global config bin-dir --absolute
#全局安装 phpunit
composer global require --dev phpunit/phpunit
#查看版本
phpunit --version

使用Composer构建你的项目

我们将新建一个unit项目用于演示单元测试的基本工作流

创建项目结构

mkdir unit && cd unit && mkdir app tests reports
#结构如下
./
├── app #存放业务代码
├── reports #存放覆盖率报告
└── tests #存放单元测试

使用Composer构建工程

#一路回车即可
composer init

#注册命名空间
vi composer.json
...
  "autoload": {
    "psr-4": {
      "App\\": "app/",
      "Tests\\": "tests/"
    }
  }
...
#更新命名空间
composer dump-autoload

#安装 phpunit 组件库
composer require --dev phpunit/phpunit

到此我们就完成项目框架的构建,下面开始写业务和测试用例。

编写测试用例

创建文件app/Example.php 这里我为节省排版就不写注释了

<?php
namespace App;

class Example
{
  private $msg = "hello world";

  public function getTrue()
  {
    return true;
  }

  public function getFalse()
  {
    return false;
  }

  public function setMsg($value)
  {
    $this->msg = $value;
  }

  public function getMsg()
  {
    return $this->msg;
  }
}

创建相应的测试文件tests/ExampleTest.php

<?php
namespace Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;
use App\Example;

class ExampleTest extends BaseTestCase
{
  public function testGetTrue()
  {
    $example = new Example();
    $result = $example->getTrue();
    $this->assertTrue($result);
  }
  
  public function testGetFalse()
  {
    $example = new Example();
    $result = $example->getFalse();
    $this->assertFalse($result);
  }
  
  public function testGetMsg()
  {
    $example = new Example();
    $result = $example->getTrue();
    // $result is world not big_cat
    $this->assertEquals($result, "hello big_cat");
  }
}

执行单元测试

[root@localhost unit]# phpunit --bootstrap=vendor/autoload.php \
tests/

PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

..F                                 3 / 3 (100%)

Time: 61 ms, Memory: 4.00MB

There was 1 failure:

1) Tests\ExampleTest::testGetMsg
Failed asserting that 'hello big_cat' matches expected true.

/opt/unit/tests/ExampleTest.php:27
/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:195
/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:148

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

这是一个非常简单的测试用例类,可以看到,执行了共3个测试用例,共3个断言,共1个失败,可以参照PHPUnit手册学习更多高级用法。

代码覆盖率

代码覆盖率反应的是测试用例测试对象行,函数/方法,类/特质的访问率是多少(PHP_CodeCoverage 尚不支持 Opcode覆盖率、分支覆盖率 及 路径覆盖率),虽然有很多人认为过分看重覆盖率是不对的,但我们初入测试还是俗气的追求一下吧。

测试覆盖率的检测对象是我们的业务代码,PHPUnit通过检测我们编写的测试用例调用了哪些函数,哪些类,哪些方法,每一个控制流程是否都执行了一遍来计算覆盖率。

PHPUnit 的覆盖率依赖 Xdebug,可以生成多种格式:

--coverage-clover <file>  Generate code coverage report in Clover XML format.
--coverage-crap4j <file>  Generate code coverage report in Crap4J XML format.
--coverage-html <dir>    Generate code coverage report in HTML format.
--coverage-php <file>    Export PHP_CodeCoverage object to file.
--coverage-text=<file>   Generate code coverage report in text format.
--coverage-xml <dir>    Generate code coverage report in PHPUnit XML format.

同时需要使用 --whitelist dir参数来设定我们需要检测覆盖率的业务代码路径,下面演示一下具体操作:

phpunit \
--bootstrap vendor/autoload.php \
--coverage-html=reports/ \
--whitelist app/ \
tests/
#查看覆盖率报告
cd reports/ && php -S 0.0.0.0:8899

使用PHPUnit进行单元测试并生成代码覆盖率报告的方法

使用PHPUnit进行单元测试并生成代码覆盖率报告的方法

这样我们就对业务代码App\Example做单元测试,并且获得我们单元测试的代码覆盖率,现在自然是百分之百,因为我的测试用例已经访问了App\Example的所有方法,没有遗漏的,开发中则能体现出你的测试时用力对业务代码测试度的完善性。

基境共享测试数据

可能你会发现我们在每个测试方法中都创建了App\Example对象,在一些场景下是重复劳动,为什么不能只创建一次然后供其他测试方法访问呢?这需要理解 PHPUnit 执行测试用例的工作流程。

我们没有办法在不同的测试方法中通过某成员属性来传递数据,因为每个测试方法的执行都是新建一个测试类对象,然后调用相应的测试方法

即测试的执行模式并不是

testObj = new ExampleTest();
testObj->testMethod1();
testObj->testMethod2();

而是

testObj1 = new ExampleTest();
testObj1->testMethod1();

testObj2 = new ExampleTest();
testObj2->testMethod2();

所以testMethod1()修改的属性状态无法传递给 testMethod2()使用。

PHPUnit则为我们提供了全面的hook接口:

public static function setUpBeforeClass()/tearDownAfterClass()//测试类构建/解构时调用
protected function setUp()/tearDown()//测试方法执行前/后调用
protected function assertPreConditions()/assertPostConditions()//断言前/后调用

当运行测试时,每个测试类大致就是如下的执行步骤

#测试类基境构建
setUpBeforeClass

#new一个测试类对象
#第一个测试用例
setUp
assertPreConditions
assertPostConditions
tearDown

#new一个测试类对象
#第二个测试用例
setUp
assertPreConditions
assertPostConditions
tearDown
...

#测试类基境解构
tearDownAfterClass

所以我们可以在测试类构建时使用setUpBeforeClass创建一个 App\Example 对象作为测试类的静态成员变量(tearDownAfterClass主要用于一些资源清理,比如关闭文件,数据库连接),然后让每一个测试方法用例使用它:

<?php
namespace Tests;

use App\Example;
use PHPUnit\Framework\TestCase as BaseTestCase;

class ExampleTest extends BaseTestCase
{
  // 类静态属性
  private static $example;

  public static function setUpBeforeClass()
  {
    self::$example = new Example();
  }

  public function testGetTrue()
  {
    // 类的静态属性更新
    self::$example->setMsg("hello big_cat");
    $result = self::$example->getTrue();
    $this->assertTrue($result);
  }

  public function testGetFalse()
  {
    $result = self::$example->getFalse();
    $this->assertFalse($result);
  }

  /**
   * 依赖 testGetTrue 执行完毕
   * @depends testGetTrue
   * @return [type] [description]
   */
  public function testGetMsg()
  {
    $result = self::$example->getMsg();
    $this->assertEquals($result, "hello big_cat");
  }
}

或者使用@depends注解来声明二者的执行顺序,并使用传递参数的方式来满足需求。

public function testMethod1()
{
  $this->assertTrue(true);
  return "hello";
}

/**
 * @depends testMethod1
 */
public function testMethod2($str)
{
  $this->assertEquals("hello", $str);
}
#执行模式大概如下
testObj1 = new Test;
$str = testObj1->testMethod1();

testObj2 = new Test;
testObj2->testMethod2($str);

理解测试执行的模式还是很有帮助的,其他高级特性请浏览官方文档。

使用phpunit.xml编排测试套件

使用测试套件来管理测试,vi phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
     backupStaticAttributes="false"
     bootstrap="./vendor/autoload.php"
     colors="true"
     convertErrorsToExceptions="true"
     convertNoticesToExceptions="true"
     convertWarningsToExceptions="true"
     processIsolation="false"
     stopOnFailure="false">
  <testsuites>
    <!--可以定义多个 suffix 用于指定待执行的测试类文件后缀-->
    <testsuite name="Tests">
      <directory suffix="Test.php">./test</directory>
    </testsuite>
  </testsuites>
  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <!--可以定义多个 对./app下的业务代码做覆盖率统计-->
      <directory suffix=".php">./app</directory>
    </whitelist>
  </filter>
  <logging>
    <!--覆盖率报告生成类型和输出目录 lowUpperBound低覆盖率阈值 highLowerBound高覆盖率阈值-->
    <log type="coverage-html" target="./reports" lowUpperBound="35" highLowerBound="70"/>
  </logging>
</phpunit>

然后直接运phpunit行即可:

[root@localhost unit]# phpunit 
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

Time: 81 ms, Memory: 4.00MB

No tests executed!

Generating code coverage report in HTML format ... done

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
无数据库的详细域名查询程序PHP版(3)
Oct 09 PHP
在PHP里得到前天和昨天的日期的代码
Aug 16 PHP
PHP 动态随机生成验证码类代码
Apr 09 PHP
深入PHP中慎用双等于(==)的详解
Jun 06 PHP
php恢复数组的key为数字序列的方法
Apr 28 PHP
PHP将Excel导入数据库及数据库数据导出至Excel的方法
Jun 24 PHP
基于PHP技术开发客服工单系统
Jan 06 PHP
PHP Cookei记录用户历史浏览信息的代码
Feb 03 PHP
PHP模板引擎Smarty内置变量调解器用法详解
Apr 11 PHP
PHP多进程编程总结(推荐)
Jul 18 PHP
yii2 url重写并隐藏index.php方法
Dec 10 PHP
PHP使用观察者模式处理异常信息的方法详解
Sep 24 PHP
ThinkPHP中图片按比例切割的代码实例
Mar 08 #PHP
PHP的微信支付接口使用方法讲解
Mar 08 #PHP
PHP实现会员账号单唯一登录的方法分析
Mar 07 #PHP
PHP模糊查询技术实例分析【附源码下载】
Mar 07 #PHP
原生PHP实现导出csv格式Excel文件的方法示例【附源码下载】
Mar 07 #PHP
PHP生成二维码与识别二维码的方法详解【附源码下载】
Mar 07 #PHP
PHP使用PDO操作sqlite数据库应用案例
Mar 07 #PHP
You might like
PHP页面间传递参数实例代码
2008/06/05 PHP
php获取地址栏信息的代码
2008/10/08 PHP
PHP实现多进程并行操作的详解(可做守护进程)
2013/06/18 PHP
thinkphp数据查询和遍历数组实例
2014/11/28 PHP
PHP SPL标准库之SplFixedArray使用实例
2015/05/12 PHP
PHP简单实现解析xml为数组的方法
2018/05/02 PHP
兼容IE/Firefox/Opera/Safari的检测页面装载完毕的脚本Ext.onReady的实现
2009/07/14 Javascript
MooBox 基于Mootools的对话框插件
2012/01/20 Javascript
JS获取单击按钮单元格所在行的信息
2014/06/17 Javascript
js实现文字跟随鼠标移动而移动的方法
2015/02/28 Javascript
jQuery实现简单的文件上传进度条效果
2020/03/26 Javascript
JavaScript编程学习技巧汇总
2016/02/21 Javascript
JS小数运算出现多为小数问题的解决方法
2016/06/02 Javascript
Knockoutjs 学习系列(一)ko初体验
2016/06/07 Javascript
详解BootStrap中Affix控件的使用及保持布局的美观的方法
2016/07/08 Javascript
详解jQuery的表单验证插件--Validation
2016/12/21 Javascript
Vue 2.0 服务端渲染入门介绍
2017/03/29 Javascript
微信小程序中做用户登录与登录态维护的实现详解
2017/05/17 Javascript
移动端滑动切换组件封装 vue-swiper-router实例详解
2018/11/25 Javascript
PWA介绍及快速上手搭建一个PWA应用的方法
2019/01/27 Javascript
深入理解使用Vue实现Context-Menu的思考与总结
2019/03/09 Javascript
Vue中遍历数组的新方法实例详解
2019/07/21 Javascript
JS实现canvas简单小画板功能
2020/06/23 Javascript
Python 单元测试(unittest)的使用小结
2018/11/14 Python
python自动识别文本编码格式代码
2019/12/26 Python
pytorch查看torch.Tensor和model是否在CUDA上的实例
2020/01/03 Python
详解HTML5中download属性的应用
2015/08/06 HTML / CSS
Reebok俄罗斯官方网上商店:购买锐步运动服装和鞋子
2016/09/26 全球购物
全世界最美丽的四星和五星级酒店预订:Prestigia.com
2017/11/15 全球购物
全球领先的全景影像品牌:Insta360
2019/08/21 全球购物
运行时异常与一般异常有何异同?
2014/01/05 面试题
学前教育毕业生自荐信范文
2013/12/24 职场文书
简短大学毕业感言
2014/01/18 职场文书
三严三实心得体会范文
2014/10/13 职场文书
Java 超详细讲解ThreadLocal类的使用
2022/04/07 Java/Android
html,css,javascript是怎样变成页面的
2023/05/07 HTML / CSS