初识 phpunit
使用laravel进行开发已经有一段时间了,可是一直没有用到laravel的测试。看文档也有点懵懵的,所以现在通过这篇文章,认真记录相关的测试过程。
文档上说PHPUnit 是一个轻量级的 PHP 测试框架,Laravel 默认就支持用 PHPUnit 来做测试,并为你的应用程序配置好了 phpunit.xml 文件,只需在命令行上运行 phpunit
就可以进行测试。
当我直接运行phpunit
命令时,出现了以下问题:
查询原因,是因为我没有将phpunit添加到环境变量当中,所以无法直接运行phpunit命令。
由于我使用的系统为mac,所以通过 vim .bash_profile
命令,在文件最后添加 export PATH="~/你的文件目录/vendor/bin:vendor/bin:$PATH"
,如果不知道文件目录,可以通过 pwd
命令查看一下。保存编辑后,运行source .bash_profile
重新加载一下配置
再运行 phpunit
命令,可查看到已正常执行了该命令了
创建测试类
查看文档:可以使用 Artisan 命令 make:test
创建一个测试用例:
// 在 Feature 目录下创建一个测试类...
php artisan make:test UserTest
// 在 Unit 目录下创建一个测试类...
php artisan make:test UserTest --unit
该命令会在 tests/Feature
目录中创建 UserTest.php
文件,我们会发现 tests 目录中有 Feature
和 Unit
两个目录,如何区分这两个目录呢?
- Unit —— 单元测试是从程序员的角度编写的。它们用于确保类的特定方法执行一组特定任务。
- Feature —— 功能测试是从用户的角度编写的。它们确保系统按照用户期望的那样运行,包括几个对象的相互作用,甚至是一个完整的 HTTP 请求。
以下内容大部分取自PHPUnit手册内容,我则根据相关内容编写一些测试例子
编写 PHPUnit 测试
针对类
Class
的测试写在类ClassTest
中。ClassTest
(通常)继承自PHPUnit\Framework\TestCase
。测试都是命名为
test*
的公用方法。也可以在方法的文档注释块(docblock)中使用
@test
标注将其标记为测试方法。在测试方法内,类似于
assertEquals()
(参见 附录 A)这样的断言方法用来对实际值与预期值的匹配做出断言。
首先通过laravel命令创建一个单元测试类 php artisan make:test CalcTest --unit
在类中添加方法,该测试类主要验证简单的加减乘除功能,按照我的理解,主要是在编写测试方法中,通过添加断言的方式,运行测试用例是否执行符合我们的预期
class CalcTest extends TestCase
{
/**
* @test
*/
public function add()
{
$a = 1;
$b = 1;
$this->assertEquals(2,($a+$b));
}
}
运行phpunit --filter=CalcTest
指定所需要运行的测试的文件类,当然也可以指定具体的测试方法phpunit --filter=add
,如果直接执行phpunit
,会运行所有的测试用例,但我这里只想运行CalcTest类下的方法,所以执行phpunit --filter=CalcTest
运行命令显示如下内容,表示我们所添加的断言通过
测试的依赖关系
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。
生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。
class CalcTest extends TestCase
{
/**
* @test
*/
public function add()
{
$a = 1;
$b = 1;
$this->assertEquals(2,($a+$b));
return $a+$b;
}
/**
* @test
* @depends add
*/
public function multiply($value)
{
$this->assertEquals(4,$value*2);
}
}
添加两个测试方法,其中multiply方法依赖于add方法,add可进行断言测试,并将返回值传递给multiply方法进行后续断言测试
执行phpunit --filter=CalcTest
命令,结果如下所示
若我修改代码,add方法的断言测试结果为错误的,并执行phpunit --filter=CalcTest
命令,结果会是怎样呢?
class CalcTest extends TestCase
{
/**
* @test
*/
public function add()
{
$a = 1;
$b = 1;
$this->assertEquals(1,($a+$b));
return $a+$b;
}
/**
* @test
* @depends add
*/
public function multiply($value)
{
$this->assertEquals(4,$value*2);
}
}
运行结果如下所示,在官方文档给出:为了快速定位缺陷,我们希望把注意力集中于相关的失败测试上。这就是为什么当某个测试所依赖的测试失败时,PHPUnit 会跳过这个测试。通过利用测试之间的依赖关系,缺陷定位得到了改进
所以multiply方法的断言测试在程序运行过程中,是跳过的。
数据供给器
测试方法可以接受任意参数。这些参数由数据供给器方法(在 例 2.5中,是 additionProvider()
方法)提供。用 @dataProvider
标注来指定使用哪个数据供给器方法。
数据供给器方法必须声明为 public
,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator
接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。
/**
* @test
* @dataProvider addition_provider
*/
public function batch_add($a,$b,$expected)
{
$this->assertEquals($expected,$a+$b);
}
public function addition_provider()
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}
在例子中,为batchAdd方法声明additionProvider为方法的数据供给器,batchAdd方法会对提供的数组中,都执行相同的断言
基境(fixture)
在编写测试时,最费时的部分之一是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。
PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp()
的模板方法。setUp()
是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown()
的模板方法。tearDown()
是清理测试所用对象的地方。
class CalcTest extends TestCase
{
protected $base;
public function setUp(): void
{
$this->base = 5;
}
/**
* @test
*/
public function equal()
{
$this->assertEquals(5,$this->base);
}
}
在测试类中定义setUp方法后,在执行测试方法前,会先调用setUp()方法,有点类似construct()方法,在创建类后,先执行construct()
实践
以上述讲述的内容,通过一个实际的例子演示编写相关测试用例
在实际开发电商项目中,我遇到需要开发活动优惠的需求,我需要在后台添加一个活动,并将需要参加活动的商品添加到活动内并设置折扣,在用户支付和购物车页面显示优惠后的价格,我根据这个需求简单的编写了相关的测试用例。
首先执行命令php artisan make:test ActivityTest --unit
生成单元测试类
单元测试类代码如下:
以下例子中,我先定义了setUp方法,这个方法实际上生成活动的数据,然后在check_user_buy_goods_is_contain_activity_goods 方法中判断是否包含活动商品,calc_buy_goods_discount_price 方法依赖于check_user_buy_goods_is_contain_activity_goods方法。在定义测试方法时,方法命名尽量语义化
class ActivityTest extends TestCase
{
protected $activityGoods;
protected $discount;
/**
* 初始化获取活动详情,这里简单赋值
*/
public function setUp(): void
{
//参与本次活动的商品id
$this->activityGoods = ['1','2','3'];
//活动的折扣
$this->discount = 0.8;
}
/**
* 检查用户购买商品是否包含活动商品
* @test
*/
public function check_user_buy_goods_is_contain_activity_goods()
{
//id为商品id,price为商品价格,num为购买数量
$cart = [
['id'=>1,'price'=>10,'num'=>1],
['id'=>2,'price'=>15,'num'=>2]
];
$cartId = collect($cart)->pluck('id')->toArray();
//是否存在活动商品
$hasActivity = array_intersect($cartId,$this->activityGoods);
$this->assertNotCount(0,$hasActivity);
return ['cart'=>$cart,'hasActivity'=>$hasActivity];
}
/**
* 计算购物车商品参与活动后的价格
* @depends check_user_buy_goods_is_contain_activity_goods
* @test
* @param $info
*/
public function calc_buy_goods_discount_price($info)
{
$cart = $info['cart'];
$hasActivity = $info['hasActivity'];
$total = 0;
//计算价格
foreach ($cart as $k=>$v){
$price = $v['price'];
$num = $v['num'];
if (in_array($v['id'],$hasActivity)){
$price = $price * $this->discount;
}
$total += $price*$num;
}
//判断折扣后的价格
$this->assertEquals(32,$total);
}
}
执行phpunit --filter=ActivityTest
命令,输出以下内容
断言测试执行成功,我们可以多测试几组数据去验证代码是否正确。
使用数据提供器
这里我直接使用calc_cart_total计算购物车总价,通过数据提供器提供数据,并验证断言是否正确
class ActivityTest extends TestCase
{
protected $activityGoods;
protected $discount;
/**
* 初始化获取活动详情,这里简单赋值
*/
public function setUp(): void
{
//参与本次活动的商品id
$this->activityGoods = ['1', '2', '3'];
//活动的折扣
$this->discount = 0.8;
}
/**
* @dataProvider addition_provider
* 计算购物车总价
* @test
* @param $input
* @param $output
*/
public function calc_cart_total($input,$output)
{
$cart = $input;
$total = 0;
$cartId = collect($cart)->pluck('id')->toArray();
$hasActivity = array_intersect($cartId, $this->activityGoods);
//计算价格
foreach ($cart as $k => $v) {
$price = $v['price'];
$num = $v['num'];
if (in_array($v['id'], $hasActivity)) {
$price = $price * $this->discount;
}
$total += $price * $num;
}
//判断折扣后的价格
$this->assertEquals($output, $total);
}
/**
* 数据提供器
* @return array[]
*/
public function addition_provider()
{
return [
[
[
['id' => 1, 'price' => 10, 'num' => 1],
['id' => 2, 'price' => 15, 'num' => 2]
],
32
],
[
[
['id' => 1 ,'price' => 10 ,'num'=>1],
['id' => 4 ,'price' => 20 ,'num'=>5]
],
108
],
[
[
['id' => 4 ,'price' => 10 ,'num'=>1],
['id' => 5 ,'price' => 20 ,'num'=>5]
],
110
]
];
}
}
执行phpunit --filter=ActivityTest
命令,输出以下内容
执行结果通过,说明代码执行正确
总结
目前这篇文章主要还都是使用phpunit进行简单的单元测试,当然还有一些其他操作,laravel的功能测试一块,后续再慢慢了解总结吧,也是希望自己在以后的开发过程中,能够更加注意测试,不要老写一些不必要的bug。
本作品采用《CC 协议》,转载必须注明作者和本文链接
不错
为啥方法名的写法有些小驼峰,有些下划线连接? :sweat_smile: