概述
对于测试的结果,可以直接在 标准输出 中看到,但是对于测试内容比较多时,标准输出 就不那么友好和直观了。
这时,我们可以借助日志文件的方式,来更加直观的分析结果。
日志内容主要分为:
- 测试结果
- xml (Junit 格式)详细结果,包括错误信息等
- text 结果的总体概要,标识了哪些方法成功,那些方法失败之类的信息
- 代码覆盖率 (需要php安装扩展:
tokenizer
和Xdebug
)- xml 基于
Clover.
所使用的 XML 日志 - text
- html 很详细的结合图形化的报告
- xml 基于
代码覆盖率
软件衡量标准
- 行覆盖率(Line Coverage)
- 按单个可执行行是否已执行到进行计量。
- 函数与方法覆盖率(Function and Method Coverage)
- 按单个函数或方法是否已调用到进行计量。仅当函数或方法的所有可执行行全部已覆盖时 PHP_CodeCoverage 才将其视为已覆盖。
- 类与特质覆盖率(Class and Trait Coverage)
- 按单个类或特质的所有方法是否全部已覆盖进行计量。仅当一个类或性状的所有方法全部已覆盖时 PHP_CodeCoverage 才将其视为已覆盖。
- Opcode 覆盖率(Opcode Coverage)(PHPUnit不支持)
- 按函数或方法对应的每条 opcode 在运行测试套件时是否执行到进行计量。一行(PHP的)代码通常会编译得到多条 opcode。进行行覆盖率计量时,只要其中任何一条 opcode 被执行就视为此行已覆盖。
- 分支覆盖率(Branch Coverage)(PHPUnit不支持)
- 按控制结构的分支进行计量。测试套件运行时每个控制结构的布尔表达式求值为 true 和 false 各自计为一个分支。
- 路径覆盖率(Path Coverage) (PHPUnit不支持)
- 按测试套件运行时函数或者方法内部所经历的执行路径进行计量。一个执行路径指的是从进入函数或方法一直到离开的过程中经过各个分支的特定序列。
- 变更风险反模式(CRAP)指数(Change Risk Anti-Patterns (CRAP) Index)
- 是基于代码单元的圈复杂度(cyclomatic complexity)与代码覆盖率计算得出的。不太复杂并具有恰当测试覆盖率的代码将得出较低的CRAP指数。可以通过编写测试或重构代码来降低其复杂性的方式来降低CRAP指数。
白名单
可以使用命令行选项:--whitelist
指定
或使用xml配置:
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
<exclude>
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
</exclude>
</whitelist>
</filter>
可以在 PHPUnit 的配置信息中设置 addUncoveredFilesFromWhitelist="true"
来将白名单中包含的所有文件全部加入到代码覆盖率报告中。这样可以把完全没有测试到的文件也一并包含到报告中。
忽略代码块
对于一些不需要测试的代码块(类、方法、或者一些行),我们可以忽略它们:
@codeCoverageIgnore
@codeCoverageIgnoreStart
与@codeCoverageIgnoreEnd
示例:
<?php
use PHPUnit\Framework\TestCase;
/**
* @codeCoverageIgnore
*/
class Foo
{
public function bar(){}
}
class Bar
{
/**
* @codeCoverageIgnore
*/
public function foo(){}
}
if (false) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
exit; // @codeCoverageIgnore
指明要覆盖的方法
-
@covers
在测试代码中用
@covers
标注来指明测试方法想要对哪些方法进行测试:/** * @covers App\Student::study */ public function testStudy(){ $this->expectOutputString('study...'); $this->student->study(); }
标注 描述 @covers ClassName::methodName
指明所标注的测试方法覆盖指定的方法。 @covers ClassName
指明所标注的测试方法覆盖给定类的全部方法。 @covers ClassName<extended>
指明所标注的测试方法覆盖给定类以及其所有父类与接口的全部方法。 @covers ClassName::<public>
指明所标注的测试方法覆盖给定类的所有 public 方法。 @covers ClassName::<protected>
指明所标注的测试方法覆盖给定类的所有 protected 方法。 @covers ClassName::<private>
指明所标注的测试方法覆盖给定类的所有 private 方法。 @covers ClassName::<!public>
指明所标注的测试方法覆盖给定类的所有非 public 方法。 @covers ClassName::<!protected>
指明所标注的测试方法覆盖给定类的所有非 protected 方法。 @covers ClassName::<!private>
指明所标注的测试方法覆盖给定类的所有非 private 方法。 @covers ::functionName
指明所标注的测试方法覆盖给定的全局函数。 -
@coversDefaultClass
指定一个默认的命名空间或类名,这样就不用在每个
@covers
标注中重复类的含命名空间的全名称/** * @coversDefaultClass App\Student */ class StudentTest extends TestCase { private $student; public function setUp() { $this->student = new Student(); } /** * @covers ::study */ public function testStudy(){ $this->expectOutputString('study...'); $this->student->study(); } }
==如果提供了此标注,则代码覆盖率信息中只考虑指定的这些方法。==
-
@coversNothing
指明所标注的测试用例不需要记录任何代码覆盖率信息
/** * @coversNothing */ public function testStudy(){ $this->expectOutputString('study...'); $this->student->study(); }
覆盖行问题
覆盖率的行的统计是按照:行,而非语句。
示例:
public function study() {
echo 'study...';
}
上面的写法会被统计为:2 行
public function study() {
echo 'study...';}
上面的写法会被统计为:1 行
public function study() {echo 'study...';}
上面的写法无法被统计:n/a 行
public function study() {
if (true) echo 'study...';
}
上面的写法无法被统计:2 行
public function study() {
if (true) {
echo 'study...';
}
}
上面的写法无法被统计:3 行
==因此:为了避免语句被覆盖,而为被正确统计,最好的方式是采用 {}
,避免缩写。==
配置详解
XML 配置文件:
<phpunit>
<!-- 配置代码覆盖率报告所使用的白名单,不配置白名单,又配置了代码覆盖率报告文件的话,会报错 -->
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">/vagrant/www/myyphp/App/</directory>
</whitelist>
</filter>
<!-- 配置测试执行期间的日志记录 -->
<logging>
<!-- 配置代码覆盖率的html报告存储位置,以及进度条颜色区间值 -->
<log type="coverage-html" target="/vagrant/www/myyphp/phpunitLog/report" lowUpperBound="35"
highLowerBound="70"/>
<!-- 代码覆盖率的xml报告存储位置 -->
<log type="coverage-clover" target="/vagrant/www/myyphp/phpunitLog/coverage.xml"/>
<!-- 生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息 -->
<log type="coverage-php" target="/vagrant/www/myyphp/phpunitLog/coverage.serialized"/>
<!-- 代码覆盖率的结果报告:直接输出 -->
<!--<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>-->
<!-- 代码覆盖率的结果报告:txt格式的 -->
<log type="coverage-text" target="/vagrant/www/myyphp/phpunitLog/coverage.txt" showUncoveredFiles="false"/>
<!--JUnit格式的测试详细结果的XML报告-->
<log type="junit" target="/vagrant/www/myyphp/phpunitLog/logfile.xml" />
<!-- 测试结果的html格式的概览结果 -->
<log type="testdox-html" target="/vagrant/www/myyphp/phpunitLog/testdox.html"/>
<!-- 测试结果的text格式的概览结果 -->
<log type="testdox-text" target="/vagrant/www/myyphp/phpunitLog/testdox.txt"/>
</logging>
</phpunit>
也可以通过命令行参数来控制:
选项 | 作用 |
---|---|
--coverage-clover |
为运行的测试生成带有代码覆盖率信息的 XML 格式的日志文件。 |
--coverage-html |
生成 HTML 格式的代码覆盖率报告 |
--coverage-text |
生成 text 格式的代码覆盖率报告 |
--coverage-crap4j |
生成 Crap4j 格式的代码覆盖率报告 |
--coverage-php |
生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息 |
--log-junit |
生成 JUnit XML 格式的日志文件 |
--testdox-html |
以 HTML 格式生成敏捷文档 |
--testdox-text |
以 纯文本 格式生成敏捷文档 |
结果示例
被测代码:
class Student
{
public function study() {
echo 'study...';
}
public function sport() {
echo 'sport...';
}
public function sleep() {
echo 'sleep...';
}
public function eat() {
echo 'eat...';
}
}
测试代码如下:
class StudentTest extends TestCase
{
private $student;
public function setUp()
{
$this->student = new Student();
}
public function testStudy(){
$this->expectOutputString('study...');
$this->student->study();
}
public function testSport(){
$this->expectOutputString('sport...');
$this->student->sport();
}
public function testSleep(){
$this->expectOutputString('sleep...');
$this->student->sleep();
}
public function testEat(){
$this->expectOutputString('eat...');
$this->student->eat();
}
}
测试结果:
.... 4 / 4 (100%)
Time: 1.52 seconds, Memory: 6.00MB
OK (4 tests, 4 assertions)
Generating code coverage report in Clover XML format ... done
Generating code coverage report in HTML format ... done
Generating code coverage report in PHP format ... done
生成的日志文件如下:
report //代码覆盖率html报告
.css
.icons
.js
dashboard.html
index.html
Student.php.html
coverage.serialized //序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息
coverage.txt //代码覆盖率的结果简要总览
coverage.xml //代码覆盖率的xml报告
logfile.xml //JUnit格式的测试详细结果的XML报告,包括错误信息
testdox.html //测试结果总概览
testdox.txt //测试结果总概览
coverage-html
coverage.txt
Code Coverage Report:
2019-01-04 10:40:42
Summary:
Classes: 100.00% (1/1)
Methods: 100.00% (4/4)
Lines: 100.00% (8/8)
\App::App\Student
Methods: 100.00% ( 4/ 4) Lines: 100.00% ( 8/ 8)
coverage.xml
<coverage generated="1546598442">
<project timestamp="1546598442">
<package name="App">
<file name="/vagrant/www/myyphp/App/Student.php">
<class name="App\Student" namespace="App">
<metrics complexity="4" methods="4" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" elements="12" coveredelements="12"/>
</class>
<line num="7" type="method" name="study" visibility="public" complexity="1" crap="1" count="1"/>
<line num="8" type="stmt" count="1"/>
<line num="9" type="stmt" count="1"/>
<line num="11" type="method" name="sport" visibility="public" complexity="1" crap="1" count="1"/>
<line num="12" type="stmt" count="1"/>
<line num="13" type="stmt" count="1"/>
<line num="15" type="method" name="sleep" visibility="public" complexity="1" crap="1" count="1"/>
<line num="16" type="stmt" count="1"/>
<line num="17" type="stmt" count="1"/>
<line num="19" type="method" name="eat" visibility="public" complexity="1" crap="1" count="1"/>
<line num="20" type="stmt" count="1"/>
<line num="21" type="stmt" count="1"/>
<metrics loc="21" ncloc="21" classes="1" methods="4" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" elements="12" coveredelements="12"/>
</file>
</package>
<metrics files="1" loc="21" ncloc="21" classes="1" methods="4" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" elements="12" coveredelements="12"/>
</project>
</coverage>
logfile.xml
<testsuites>
<testsuite name="Tests\StudentTest" file="/vagrant/www/myyphp/Tests/StudentTest.php" tests="4" assertions="4" errors="0" failures="0" skipped="0" time="0.153877">
<testcase name="testStudy" class="Tests\StudentTest" classname="Tests.StudentTest" file="/vagrant/www/myyphp/Tests/StudentTest.php" line="22" assertions="1" time="0.152232"/>
<testcase name="testSport" class="Tests\StudentTest" classname="Tests.StudentTest" file="/vagrant/www/myyphp/Tests/StudentTest.php" line="26" assertions="1" time="0.000371"/>
<testcase name="testSleep" class="Tests\StudentTest" classname="Tests.StudentTest" file="/vagrant/www/myyphp/Tests/StudentTest.php" line="30" assertions="1" time="0.000719"/>
<testcase name="testEat" class="Tests\StudentTest" classname="Tests.StudentTest" file="/vagrant/www/myyphp/Tests/StudentTest.php" line="34" assertions="1" time="0.000555"/>
</testsuite>
</testsuites>
testdox.html
testdox.txt
Student
[x] Study
[x] Sport
[x] Sleep
[x] Eat