先发下简书的干货:

教你一步一步写一个phpunit testcase:https://www.jianshu.com/p/ba6829a6f3ec

程序地址

https://github.com/yezuozuo/how-to-write-a-phpunit-testcase

使用方法

composer install
phpunit tests/EventTest.php

  

背景

https://phpunit.de/manual/current/zh_cn/index.html 这个是phpunit的文档,但是看完了我完全不知道怎么入手好吗,从0到1这个过程就在下面。

综述

目录结构

.
|-- reports
|-- src
| -- PHPUnitEventDemo
| -- Event.php
| -- EventException.php
| -- User.php
|-- tests
| -- EventTest.php
|-- .gitignore
|-- composer.json
|-- phpunit.xml
|-- README.md
PHPUnitEventDemo - 下面是要测试的类
Event.php - Event类
EventException.php - Event异常类
User.php - User类
tests - 单元测试目录
EventTest.php - 测试Event类的测试用例

  

Assertions(断言)

断言为PHPUnit的主要功能,用来验证单元的执行结果是不是预期值。

例子:

assertTrue(true);   # SUCCESSFUL
assertEquals('orz', 'oxz', 'The string is not equal with orz'); #UNSUCCESSFUL
assertCount(1, array('Monday')); # SUCCESSFUL
assertContains('PHP', array('PHP', 'Java', 'Ruby')); # SUCCESSFUL

assertTrue():判断实际值是否为true。

assertEquals():预期值是orz,实际值是oxz,因为两个值不相等,所以这一个断言失败,会显示The string is not equal with orz的字串。

assertCount():预期数组大小为1。

assertContains():预期数组中有一个PHP字串的元素存在。

从上面的后三个assertions可以发现,预期值都是在第一个参数,而后面则是实际值。

代码示例

src/PHPUnitEventDemo/User.php

<?php

namespace PHPUnitEventDemo;

/**
* Class User
*
* @package PHPUnitEventDemo
*/
class User {
/**
* @var int 用户id
*/
public $id; /**
* @var string 用户名
*/
public $name; /**
* @var string 用户邮箱
*/
public $email; /**
* User constructor.
*
* @param $id
* @param $name
* @param $email
*/
public function __construct($id, $name, $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
}

  

User类很单纯,主要就是建立User对象。

src/PHPUnitEventDemo/Event.php

<?php

namespace PHPUnitEventDemo;

/**
* Class Event
*
* @package PHPUnitEventDemo
*/
class Event {
/**
* @var int event id
*/
public $id; /**
* @var string 事件名
*/
public $name; /**
* @var string 事件开始时间
*/
public $startDate; /**
* @var string 事件结束时间
*/
public $endDate; /**
* @var int 参加者限制
*/
public $attendLimit; /**
* @var array 参加者列表
*/
public $attendArr = array(); /**
* Event constructor.
*
* @param $id
* @param $name
* @param $startDate
* @param $endDate
* @param $attendLimit
*/
public function __construct($id, $name, $startDate, $endDate, $attendLimit) {
$this->id = $id;
$this->name = $name;
$this->startDate = $startDate;
$this->endDate = $endDate;
$this->attendLimit = $attendLimit;
} /**
* 用户报名,将报名的用户存在数组中,数组的索引值就是用户的id
*
* @param $user
* @return bool
* @throws EventException
*/
public function reserve($user) {
// 报名人数是否超过限制
if ($this->attendLimit > $this->getAttendNumber()) {
if (array_key_exists($user->id, $this->attendArr)) {
throw new EventException('Duplicated reservation', EventException::DUPLICATED_RESERVATION);
}
// 使用者报名
$this->attendArr[$user->id] = $user; return true;
} return false;
} /**
* 获取报名用户的人数
*
* @return int
*/
public function getAttendNumber() {
return sizeof($this->attendArr);
} /**
* 取消报名
*
* @param $user
*/
public function unReserve($user) {
unset($this->attendArr[$user->id]);
}
}

  

Event类就是用来让用户报名的,也很简单。

接着我们需要写EventTest来测试Event的单元测试结果是不是符合预期。

<?php

/**
* Class EventTest
*/
class EventTest extends PHPUnit_Framework_TestCase {
/**
* @var PHPUnitEventDemo\Event 事件
*/
private $event; /**
* @var PHPUnitEventDemo\User 用户
*/
private $user; public function setUp() {
$eventId = 1;
$eventName = '活动1';
$eventStartDate = '2016-11-01 18:00:00';
$eventEndDate = '2016-11-01 20:00:00';
$eventAttendLimit = 10;
$this->event = new \PHPUnitEventDemo\Event($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit); $userId = 1;
$userName = 'User1';
$userEmail = 'user1@zoco.space';
$this->user = new \PHPUnitEventDemo\User($userId, $userName, $userEmail);
} public function tearDown() {
$this->event = null;
$this->user = null;
} /**
* 测试报名
*
* @return array
*/
public function testReserve() {
$this->event->reserve($this->user);
$expectedNumber = 1; // 预期报名人数
$this->assertEquals($expectedNumber, $this->event->getAttendNumber()); // 报名清单中有已经报名的人
$this->assertContains($this->user, $this->event->attendArr); return [$this->event, $this->user];
} /**
* 测试取消报名
*
* @param $obj
* @depends testReserve
*/
public function testUnReserve($obj) {
$event = $obj[0];
$user = $obj[1]; // 使用者取消报名
$event->unReserve($user); $unReserveExpectedCount = 0; // 预期报名人数
$this->assertEquals($unReserveExpectedCount, $event->getAttendNumber()); // 报名清单中没有已经取消报名的人
$this->assertNotContains($user, $event->attendArr);
} /**
* @param $eventId
* @param $eventName
* @param $eventStartDate
* @param $eventEndDate
* @param $eventAttendLimit
* @dataProvider eventsDataProvider
*/
public function testAttendeeLimitReserve($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit) {
// 测试报名人数限制
$event = new \PHPUnitEventDemo\Event($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit);
$userNumber = 6; // 建立不同使用者报名
for ($userCount = 1; $userCount < $userNumber; $userCount++) {
$userId = $userCount;
$userName = 'User ' . $userId;
$userEmail = 'user' . $userId . '@zoco.space';
$user = new \PHPUnitEventDemo\User($userId, $userName, $userEmail); $reservedResult = $event->reserve($user); // 报名人數是否超过
if ($userCount > $eventAttendLimit) {
// 无法报名
$this->assertFalse($reservedResult);
} else {
$this->assertTrue($reservedResult);
}
}
} /**
* @return array
*/
public function eventsDataProvider() {
$eventId = 1;
$eventName = '活动1';
$eventStartDate = '2016-11-01 12:00:00';
$eventEndDate = '2016-11-01 13:00:00';
$eventAttendeeLimitNotFull = 5;
$eventAttendeeFull = 10; $eventsData = array(
array(
$eventId,
$eventName,
$eventStartDate,
$eventEndDate,
$eventAttendeeLimitNotFull
),
array(
$eventId,
$eventName,
$eventStartDate,
$eventEndDate,
$eventAttendeeFull
)
); return $eventsData;
} /**
* @expectedException \PHPUnitEventDemo\EventException
* @expectedExceptionMessage Duplicated reservation
* @expectedExceptionCode 1
*/
public function testDuplicatedReservationWithException() {
// 测试重复报名,预期丢出异常
// 同一个使用者报名两次
$this->event->reserve($this->user);
$this->event->reserve($this->user);
}
}

  

EventTest会继承phpunit的类PHPUnit_Framework_TestCase。

EventTest内有一个测试用例testReserve()。

testReserve()内主要会建立一个用户及事件,使用者去报名一个活动,所以活动已经有一个人报名了。

接下来的断言,assertEquals()会预期活动报名人数有1个人。

assertContains()预期在活动报名清单内,已经有已报名的使用者。

其中有一个@depends testReserve,这个叫做依赖测试。

依赖测试,如果有两个测试用例,具有依赖关系,就可以使用测试依赖在两个测试用例建立依赖关系。

这里将报名与取消报名分成两个测试用例,让取消报名的测试依赖于报名的测试。

执行测试

➜  how-to-write-a-phpunit-testcase git:(master) ✗ phpunit --bootstrap vendor/autoload.php tests/EventTest.php
PHPUnit 5.4.8 by Sebastian Bergmann and contributors. ..... 5 / 5 (100%) Time: 78 ms, Memory: 10.00MB OK (5 tests, 17 assertions)

  

Producer 与 Consumer

testUnReserve()在注释内利用@depends testReserve()标记依赖于testReserve()测试,而被依赖的测试可以当作producer,将值传给依赖的测试testUnReserve()为consumer,通过参数接收。

这样就能够报名testReserve()与取消报名testUneserve()测试分开,testUneserve()会接收来自testReserve()的返回值,为一个两个元素的数组,数组的第一个元素为,已经有人报名的对象,第二个元素为用户对象,是已经报名的使用者。

Q:如果testReserve()执行失败,testUnReserve()会执行吗?

A:是不会的,当被依赖的测试案例如果测试失败,那依赖的测试就会忽略执行。

我们可以试着将testReserve()故意测试失败,只需要针对事件物件的getAttendNumber()断言的预期值,从1改成0就可以让testReserve()测试失败,接着再执行测试:

➜  how-to-write-a-phpunit-testcase git:(master) ✗ phpunit --bootstrap vendor/autoload.php tests/EventTest.php
PHPUnit 5.4.8 by Sebastian Bergmann and contributors. FS.. Time: 110 ms, Memory: 10.00MB There was 1 failure: 1) EventTest::testReserve
Failed asserting that 1 matches expected 0. /Users/wangzhihao/git/how-to-write-a-phpunit-testcase/tests/EventTest.php:52 FAILURES!
Tests: 4, Assertions: 14, Failures: 1, Skipped: 1.

  

Data Providers(数据提供者)

数据提供者,能提供多次的测试数据进行多次的测试。

使用数据提供者,能让测试更简洁,因为,可以将测试的断言与测试数据分开写。

在EventTest内增加一个testDuplicatedReservationWithException()测试用例,在注释内标注:

@expectedException \PHPUnitEventDemo\EventException 预期的异常类。
@expectedExceptionMessage 预期的异常消息。
@expectedExceptionCode 预期的异常代码。

  

也就是,预期在这个测试用例内会接收到EventException的异常类别,异常消息为预留的值,异常代码为1。

数据提供者为:

 public function eventsDataProvider() {
$eventId = 1;
$eventName = '活动1';
$eventStartDate = '2016-11-01 12:00:00';
$eventEndDate = '2016-11-01 13:00:00';
$eventAttendeeLimitNotFull = 5;
$eventAttendeeFull = 10; $eventsData = array(
array(
$eventId,
$eventName,
$eventStartDate,
$eventEndDate,
$eventAttendeeLimitNotFull
),
array(
$eventId,
$eventName,
$eventStartDate,
$eventEndDate,
$eventAttendeeFull
)
); return $eventsData;
}

  

在数据提供者的基础上进行对报名人数限制的测试:

public function testAttendeeLimitReserve($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit) {
// 测试报名人数限制
$event = new \PHPUnitEventDemo\Event($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit);
$userNumber = 6; // 建立不同使用者报名
for ($userCount = 1; $userCount < $userNumber; $userCount++) {
$userId = $userCount;
$userName = 'User ' . $userId;
$userEmail = 'user' . $userId . '@zoco.space';
$user = new \PHPUnitEventDemo\User($userId, $userName, $userEmail); $reservedResult = $event->reserve($user); // 报名人數是否超过
if ($userCount > $eventAttendLimit) {
// 无法报名
$this->assertFalse($reservedResult);
} else {
$this->assertTrue($reservedResult);
}
}
}

  

Fixtures

Fixture能协助建立测试时需要用到的测试环境,对象的建立,在测试完后,把测试环境,对象析构掉,还原到初始化前的状态。

主要透过setUp()与tearDown()分别来初始化测试与还原到初始化前的状态。

代码如下:

public function setUp() {
$eventId = 1;
$eventName = '活动1';
$eventStartDate = '2016-11-01 18:00:00';
$eventEndDate = '2016-11-01 20:00:00';
$eventAttendLimit = 10;
$this->event = new \PHPUnitEventDemo\Event($eventId, $eventName, $eventStartDate, $eventEndDate, $eventAttendLimit); $userId = 1;
$userName = 'User1';
$userEmail = 'user1@zoco.space';
$this->user = new \PHPUnitEventDemo\User($userId, $userName, $userEmail);
} public function tearDown() {
$this->event = null;
$this->user = null;
}

  

把$event,$user类修改成全局变量,接着把构造类写在setUp()中,析构类写在tearDown(),testReserve()与testDuplicatedReservationWithException中使用这两个变量。

所以在执行测试的时候,运行顺序会是:

setUp()->testReserve()->tearDown()->...->setUp()->testDuplicatedReservationWithException

  

设定phpunit

在前面使用phpunit工具来执行测试时,有用到--bootstrap,在执行测试前先执行vendor/autoload.php来注入自动加载的功能。但是每次执行测试,都要加上参数有点麻烦,phpunit可以使用XML来设定测试。

phpunit.xml的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="../vendor/autoload.php" verbose="true">
<testsuite>
<directory suffix="Test.php">../tests</directory>
</testsuite> <filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory>
</whitelist>
</filter>
</phpunit>

  

加入bootstrap和filter屬性。

执行测试,如果XML名字不是phpunit.xml的话,可以利用--configuration来指定。

直接执行,结果如下:

➜  how-to-write-a-phpunit-testcase git:(master) ✗ phpunit tests/EventTest.php
PHPUnit 5.4.8 by Sebastian Bergmann and contributors. Runtime: PHP 7.0.12 with Xdebug 2.4.0
Configuration: /Users/wangzhihao/git/how-to-write-a-phpunit-testcase/phpunit.xml ..... 5 / 5 (100%) Time: 101 ms, Memory: 10.00MB OK (5 tests, 17 assertions)

  

还有更多的phpunit.xml在这里https://phpunit.de/manual/current/en/appendixes.configuration.html

Code Coverage 分析

写好单元测试之后,该如何了解到哪些程序还没有经过测试?目标程序被测试百分比有多少?

phpunit利用PHP CodeCoverage來计算程序代码覆盖率(code coverage),需要安裝 Xdebug。

该如何产生Code coverage呢?

先在项目下建立一个reports/目录,存放code coverage分析的结果。

然后执行

phpunit --coverage-html reports/ tests/

  

或者执行

phpunit --bootstrap vendor/autoload.php --coverage-html reports/ tests/

  

当然,也可以使用XML来设定。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php" verbose="true">
<testsuite>
<directory suffix="Test.php">./tests</directory>
</testsuite> <filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter> <logging>
<log type="coverage-html" target="./report" charset="UTF-8"/>
</logging>
</phpunit>

  

接着执行测试:

phpunit tests/EventTest.php

  

就可以在reports/下打开index.html或其他html页面,浏览code coverage分析的结果。

如图所示:

 
1.jpeg
 
2.jpeg

结束。


作者:_叶左左
链接:https://www.jianshu.com/p/ba6829a6f3ec
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

PHPUnit_Framework_Assert单元测试的更多相关文章

  1. 单元测试 2 & 初识模块3

    单元测试 - 创建测试用例 单元测试是什么? (老鸟可以无视下面这段话.) hi,新同学们,咱们的PHP代码里满布着好多函数和类,经常互相调用,你改的一个函数/方法可能是"比较底层" ...

  2. Intellij idea添加单元测试工具

    1.idea 版本是14.0.0 ,默认带有Junit,但是不能自动生成单元测试,需要下载JunitGererator2.0插件 2.Settings -Plugins,下载 JunitGenerat ...

  3. Python的单元测试(二)

    title: Python的单元测试(二) date: 2015-03-04 19:08:20 categories: Python tags: [Python,单元测试] --- 在Python的单 ...

  4. Python的单元测试(一)

    title: Python的单元测试(一) author: 青南 date: 2015-02-27 22:50:47 categories: Python tags: [Python,单元测试] -- ...

  5. javascript单元测试框架mochajs详解

    关于单元测试的想法 对于一些比较重要的项目,每次更新代码之后总是要自己测好久,担心一旦上线出了问题影响的服务太多,此时就希望能有一个比较规范的测试流程.在github上看到牛逼的javascript开 ...

  6. 使用NUnit为游戏项目编写高质量单元测试的思考

    0x00 单元测试Pro & Con 最近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要.这篇博客就来聊一聊这段时间的感悟和想法.由于游戏开发和传统软 ...

  7. 我这么玩Web Api(二):数据验证,全局数据验证与单元测试

    目录 一.模型状态 - ModelState 二.数据注解 - Data Annotations 三.自定义数据注解 四.全局数据验证 五.单元测试   一.模型状态 - ModelState 我理解 ...

  8. ABAP单元测试最佳实践

    本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...

  9. python_单元测试unittest

    Python自带一个单元测试框架是unittest模块,用它来做单元测试,它里面封装好了一些校验返回的结果方法和一些用例执行前的初始化操作. 步骤1:首先引入unittest模块--import un ...

随机推荐

  1. App Distribution Guide (二)

    Configuring Your Xcode Project for Distribution  You can edit your project settings anytime, but som ...

  2. Visual Studio Debug和Release的区别及obj的作用

    一.Debug和Release的区别      1.Debug:调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂), ...

  3. JAVA常见算法题(九)

    package com.xiaowu.demo; /** * * 一个数如果恰好等于它的因子之和,这个数就称为"完数".例如6=1+2+3.编程找出1000以内的所有完数. * * ...

  4. memcache学习资料

    memcached是国外社区网站LiveJournal团队开发,通过缓存数据库查询结果,减少数据库访问次数,从而提高动态web站点性能.官方站点 http://memcached.org/memcac ...

  5. CSS从大图片上截取小图标

    一张图片,用CSS分割成多个小图标. css样式: .icon{ background:url(../images/tabicons.png) no-repeat;width:18px; line-h ...

  6. pc_lint的用法转

    PC-Lint是一款C/C++软件代码静态分析工具,不仅可以检查一般的语法错误,还可以检查潜在的错误,比如数组访问越界.内存泄漏.使用未初始化变量.使用空指针等.在单元测试前使用PC-Lint来检查代 ...

  7. WCF 404.3 MIME 映射错误

    WCF部署在IIS下,报错如下: HTTP 错误 404.3 - Not Found由于扩展配置问题而无法提供您请求的页面.如果该页面是脚本,请添加处理程序.如果应下载文件,请添加 MIME 映射. ...

  8. How to Check some table was locked

    select * from sys.sysprocesses where blocked<>0  看看waittime是不是很大  kill spid

  9. TCO'10 Wildcard Round 1000pt

    题目大意: 给定一个N*M的棋盘,棋子可以攻击其左右距离不超过K的棋子.问有多少种放法使得棋盘上的棋子不能互相攻击. N,M,K都在1到1000000000的范围内,结果对100003取模. 官方题解 ...

  10. 谁动了我的cpu——oprofile使用札记(转)

    引言 cpu无端占用高?应用程序响应慢?苦于没有分析的工具? oprofile利用cpu硬件层面提供的性能计数器(performance counter),通过计数采样,帮助我们从进程.函数.代码层面 ...