专栏首页php专栏不会Phpunit单元测试PHPer写不出高质量的代码
原创

不会Phpunit单元测试PHPer写不出高质量的代码

单元测试

单元测试是指检查和验证软件中最小的可测试单元。单元是要测试的最小功能模块。单元测试是软件开发过程中要进行的最低级别的测试活动。软件的独立单元将与程序的其他部分隔离测试。

在PHP中,最小的单元可以引用函数或类。需要验证的是每个函数,每个类的函数都符合我们的期望。

单元测试是什么意思

它可以减少一些细节错误的发生,如错误报告时是否没有错误,输入参数和结果是否符合要求等。

便于今后的改造和维护。在实际工作中,有很多情况需要制作一个版本的函数,但是内部的细节需要在上线后进行调整。如果有一个单元测试,那么更改它会更放心,改进单元测试的过程也是进一步理解需求的过程。

对于平时无法到达的异常分支,更容易找到,并且该分支的处理逻辑可以通过人工测试采取很多步骤来达到,节省了时间

最近,我还尝试为开发中的函数编写单元测试,并意识到了单元测试的好处。在需求中有一个更复杂的时间计算逻辑。一开始,我以为各种情况都是经过深思熟虑的,然后就写了下来。然而,在运行了首先编写的单元测试之后,我仍然发现了几个隐藏的深层次问题我相信我也可以得到它们通过了测试。

问题解决后,在提出测试的过程中需要进行更改。许多关键代码需要更改。通常情况下,由于数据库需要查找各种数据来运行接口,因此很难进行自检,如果数据不能更改,则必须重新运行接口自检。但是,在这次正确地指定了单元测试之后,我们可以根据自己的想法安全大胆地转换代码。经过代码更改、测试运行、代码更改和测试运行的循环,我们很快交付了需求。

单元测试的一些概念

我以前也接触过PHP、python、JS和其他语言,我对这些语言的单元测试有一定的了解。接下来,我将介绍单元测试中的一些常见概念。

断言

为了更详细地理解断言,我推荐一个博客:https://www.jianshu.com/p/9b8c88deed6a

在软件测试中,特别是在单元测试中,一个必要的功能是“断言”。顾名思义,在编写程序时,通常会做出某些假设,即断言用于捕获假设异常。

下面举个例子:

一个简单的函数add有两个参数。它的功能是返回两个参数的和。当我需要验证这个函数的正确性时,我需要模拟两个输入参数,并确定函数的返回值是否是两个输入参数的和。确定返回值是否准确的过程称为断言。

function add($a, $b)
{
    return $a + $b;
}

基境

每一个单元测试方法都是一个独立的个体,每次单元测试完毕,需要将数据恢复到正确的状态中,不至于被其他测试方法给影响。

在phpunit中,给出的 TestCase 基类即有两个方法, setUp 和 setDown 分别用于为每个单元测试创建测试对象和清理测试对象

数据供给器

对同一类情况进行测试,通常可以用数据供给器传入不同入参和相应的预期返回值。

测试方法可以接受任意参数。这些参数由数据供给器方法提供。在phpunit中使用 @dataProvider 标注来指定使用哪个数据供给器方法。

php如何集成单元测试

PHP的单元测试依赖一个测试框架:phpunit(官方文档:https://phpunit.readthedocs.io/zh_CN/latest/index.html )

如何安装

可以通过phar的方式安装

$ wget https://phar.phpunit.de/phpunit-7.0.phar $ chmod +x phpunit-7.0.phar $ sudo mv phpunit-7.0.phar /usr/local/bin/phpunit $ phpunit --version

也可以通过 composer 进行统一管理

$ composer require phpunit/phpunit

在 composer.json 中会出现如下依赖

{
    "require": {
        "phpunit/phpunit": "^7.5"
    }
}

并且会出现 vendor/bin/phpunit 文件,直接运行即可

如何编写单元测试

所有类需要继承 PHPUnit\Framework\TestCase , setUp 函数用于初始化测试对象, setDown 函数用于清理测试对象,更多规范

更具体写法可以查看底部的 举个栗子

phpunit常用断言方法

更多断言方法详见 phpunit 官方文档,基本都能顾名思义。

断言函数

作用

示例

assertEquals(\$except, \$value)

断言相等

$this->assertEquals(2, 1 + 1)

assertEmpty($value)

断言为空

$this->assertEmpty([])

assertNotEmpty($value)

断言不为空

$this->assertNotEmpty([1, 2, 3])

assertTrue($value)

断言为真

$this->assertTrue(1 === 1)

assertFalse($value)

断言为假

$this->assertFalse(1 === ‘1’)

expectException(\Exception $e)

断言本次测试会出现特定异常

$this->expectException(\Exception::class); throw new \Exception(‘测试’, 1002);

expectExceptionCode($code)

断言异常状态码

$this->expectExceptionCode(1002); throw new \Exception(‘测试’, 1002);

expectExceptionMessage($msg)

断言异常信息

$this->expectExceptionMessage(‘测试’); throw new \Exception(‘测试’, 1002);

expectOutputString($msg)

断言输出

$this->expectOutputString(‘Hello’);echo “Hello”;

getActualOutput()

获取实际输出

如何运行单元测试

# 运行全部测试
phpunit
# 运行某个分组的单元测试
phpunit --group GroupA
# 运行指定测试类的所有测试用例
phpunit tests/xxxxTest.php
# 运行所有测试类中满足filter条件的方法
phpunit --filter xxxFunc
# 运行某个测试类中满足filter条件的

phpunit.xml 是什么

phpunit.xml 是一个XML格式的配置文件,能够配置单元测试中的一些默认行为,比如环境变量、启动文件、日志记录等,官方文档如下 https://phpunit.readthedocs.io/zh_CN/latest/configuration.html

一个样例配置如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!--phpunit标签是配置中的核心,这里配置了启动文件 "./tests/bootstrap.php"-->
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="./tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <!--测试套件:非常多的测试用例放在一起即可成为测试套件,执行时会扫描包含的所有 *Test.php文件-->
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <!--这里配置了白名单,只有这里边的代码会被统计覆盖率-->
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app/library</directory>
            <directory suffix=".php">./app/models</directory>
        </whitelist>
    </filter>
    <!--这里配置了PHP的环境变量-->
    <php>
        <server name="APP_ENV" value="product"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
    </php>
    <!--这里是日志记录,把覆盖率信息保存到 ./tests/codeCoverage-->
    <logging>
        <log type="coverage-html" target="./tests/codeCoverage"/>
    </logging>
</phpunit>

如何查看代码覆盖率

执行 phpunit 之后,根据 <logging> 中的配置,会自动生成代码覆盖率信息至 ./tests/codeCoverage/ ,打开其中 index.html 即可查看覆盖率信息。

举个例子:

以一个简单的斐波拉契数列计算函数为例

斐波那契数列由0和1开始,之后的斐波那契系数就是由之前的两数相加而得出。

输入输出分析

根据函数特点,我们可以通过验证已知情况和特殊情况的方式去验证,经过分析结果如下

正常输入的已知情况:

入参

预期返回

描述

0

0

规则

1

1

规则

2

1

0 + 1 = 1

3

2

1 + 1 = 2

4

3

1 + 2 = 3

5

5

2 + 3 = 5

6

8

3 + 5 = 8

12

144

55 + 89 = 144

异常输入的情况处理

处理为0,或者抛出异常均可

入参

预期返回

描述

-1

0

非正常输入处理为0

0

非正常输入处理为0

1.1

0

非正常输入处理为0

‘文字’

0

非正常输入处理为0

编写测试类

tests/FunctionTest.php

use PHPUnit\Framework\TestCase;
class FunctionsTest extends TestCase
{
    /**
     * @dataProvider fibon_normal_provider
     * @param $input
     * @param $except
     */
    public function test_fibon_normal($input, $except)
    {
        $this->assertEquals($except, fibon($input));
    }

    public function fibon_normal_provider()
    {
        return [
            [0, 0],
            [1, 1],
            [2, 1],
            [3, 2],
            [4, 3],
            [5, 5],
            [6, 8],
            [12, 144],
        ];
    }

    /**
     * @dataProvider fibon_error_provider
     * @param $input
     * @param $except
     */
    public function test_fibon_error($input, $except)
    {
        $this->assertEquals($except, fibon($input));
    }

    public function fibon_error_provider()
    {
        return [
            [-1, 0],
            [1.1, 0],
            ['', 0],
            ['文字', 0],
        ];
    }
}

函数功能实现

(PS:此法效率很差,约莫是 O(n^2) 的复杂度,仅用于此处演示)

functions.php

function fibon($a)
{
    if (!is_int($a)) {
        return 0;
    }
    if ($a <= 0) {
        return 0;
    } elseif ($a == 1) {
        return 1;
    } else {
        return fibon($a - 1) + fibon($a - 2);
    }
}

运行结果

vagrant@homestead:~/code/bmtrip/platoReceivable$ phpunit tests/FunctionsTest.php --filter test_fibon
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

............                                                      12 / 12 (100%)

Time: 5.77 seconds, Memory: 26.00 MB

OK (12 tests, 12 assertions)

Generating code coverage report in HTML format ... done

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作

    以前写过ASP.NET Core 2.x的REST API文章,今年再更新一下到3.0版本。

    solenovex
  • 四种高性能数据类型,Python collections助你优化代码、简洁任务

    Python 的最大优势之一就是它有各种各样的模块和软件包可供选择。这些模块和包将 Python 的功能扩展到了许多流行领域,包括机器学习、数据科学、Web 开...

    CDA数据分析师
  • PHPUnit 单元测试都不会的 PHPer 没法写出高质量的代码

    单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级...

    猿哥
  • springboot 整合redis

    上面就是简单的实现redis的存数据,取数据。具体怎么用看你们,如果redis不会安装可以去看看简单的安装教程。这里就不一一描述了。

    斯文的程序
  • [UWP]使用SpringAnimation创建有趣的动画

    最近用弹簧动画(SpringAnimation)做了两个番茄钟,关于弹簧动画官方文档已经介绍得够详细了,这篇文章就摘录一些官方文档核心内容。

    dino.c
  • MySQL的几个知识点

    今天上班没搞什么新的东西,所以简单写点儿MySQL相关的小的tip,希望对大家有所帮助吧,如果你恰好了解这些功能,那权当我没说过。

    AsiaYe
  • kubernetes CSI存储插件探究

    本周帮助为一个kubernetes CSI插件实现了动态供应(dynamic provisioning)功能,在这个过程中学习并了解了kubernetes CS...

    jeremyxu
  • MySQL中的too many connections错误

    今天中午,开发测试环境的MySQL服务报了一个too many connections的错误,从问题上看,可能是连接池被打满了,导致所有的连接都不可用了。

    AsiaYe
  • MySQL动态修改复制过滤器

    今天是周五,最近睡眠不好,一整天都浑浑噩噩的,状态不是很好,周五了,准备早点回家,早点休息了,今天的内容写写线上的一个案例,主要是关于主从复制过程中的r...

    AsiaYe
  • 四种高性能数据类型,Python collections助你优化代码、简洁任务

    Python 的最大优势之一就是它有各种各样的模块和软件包可供选择。这些模块和包将 Python 的功能扩展到了许多流行领域,包括机器学习、数据科学、Web 开...

    用户2769421

扫码关注云+社区

领取腾讯云代金券