初识 phpunit

使用laravel进行开发已经有一段时间了,可是一直没有用到laravel的测试。看文档也有点懵懵的,所以现在通过这篇文章,认真记录相关的测试过程。

文档上说PHPUnit 是一个轻量级的 PHP 测试框架,Laravel 默认就支持用 PHPUnit 来做测试,并为你的应用程序配置好了 phpunit.xml 文件,只需在命令行上运行 phpunit 就可以进行测试。

当我直接运行phpunit命令时,出现了以下问题:
认真实践学习 Laravel phpunit

查询原因,是因为我没有将phpunit添加到环境变量当中,所以无法直接运行phpunit命令。

由于我使用的系统为mac,所以通过 vim .bash_profile 命令,在文件最后添加 export PATH="~/你的文件目录/vendor/bin:vendor/bin:$PATH",如果不知道文件目录,可以通过 pwd命令查看一下。保存编辑后,运行source .bash_profile 重新加载一下配置

再运行 phpunit 命令,可查看到已正常执行了该命令了

实践 Laravel phpunit

创建测试类

查看文档:可以使用 Artisan 命令 make:test 创建一个测试用例:

// 在 Feature 目录下创建一个测试类...
php artisan make:test UserTest

// 在 Unit 目录下创建一个测试类...
php artisan make:test UserTest --unit

该命令会在 tests/Feature 目录中创建 UserTest.php 文件,我们会发现 tests 目录中有 FeatureUnit 两个目录,如何区分这两个目录呢?

  • Unit —— 单元测试是从程序员的角度编写的。它们用于确保类的特定方法执行一组特定任务。
  • Feature —— 功能测试是从用户的角度编写的。它们确保系统按照用户期望的那样运行,包括几个对象的相互作用,甚至是一个完整的 HTTP 请求。

以下内容大部分取自PHPUnit手册内容,我则根据相关内容编写一些测试例子

编写 PHPUnit 测试

  1. 针对类 Class 的测试写在类 ClassTest中。

  2. ClassTest(通常)继承自 PHPUnit\Framework\TestCase

  3. 测试都是命名为 test* 的公用方法。

    也可以在方法的文档注释块(docblock)中使用 @test 标注将其标记为测试方法。

  4. 在测试方法内,类似于 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

运行命令显示如下内容,表示我们所添加的断言通过

实践 Laravel phpunit

测试的依赖关系

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命令,结果如下所示

实践 Laravel phpunit

若我修改代码,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方法的断言测试在程序运行过程中,是跳过的。

实践 Laravel phpunit

数据供给器

测试方法可以接受任意参数。这些参数由数据供给器方法(在 例 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方法会对提供的数组中,都执行相同的断言

实践 Laravel phpunit

基境(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命令,输出以下内容

实践 Laravel phpunit

断言测试执行成功,我们可以多测试几组数据去验证代码是否正确。

使用数据提供器

这里我直接使用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命令,输出以下内容

实践 Laravel phpunit

执行结果通过,说明代码执行正确

总结

目前这篇文章主要还都是使用phpunit进行简单的单元测试,当然还有一些其他操作,laravel的功能测试一块,后续再慢慢了解总结吧,也是希望自己在以后的开发过程中,能够更加注意测试,不要老写一些不必要的bug。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

为啥方法名的写法有些小驼峰,有些下划线连接? :sweat_smile:

3年前 评论
oliver-l (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!