从基于 PHPUnit 编写单元测试开始


为什么要做测试

我们知道,Laravel 框架开箱提供的测试功能是基于 PHPUnit 实现的,PHPUnit 是 PHP 语言中最负盛名的单元测试框架。在介绍 Laravel 框架提供的测试功能之前,我们先从源头 PHPUnit 开始,聊聊如何在 PHP 框架中实现单元测试。

即使是很小规模的项目开发也需要数小时的辛苦编码。在开发过程中,应用代码中或多或少都会存在一些大大小小的问题,开发者往往会尝试在开发过程中解决这些问题,以便顺利上线。但问题是,在未经完整测试的情况下,没有一种方式可以确保最终上线代码中没有任何问题,此外,也没有办法确保修复老问题的代码会不会引发新问题。

为了解决上述问题,我们需要在开发过程中引入测试流程,并将其作为日常开发流程中不可或缺的一部分,从而确保代码质量。在现代软件开发流程中,测试驱动开发、持续交付、持续集成这些概念中都将测试作为开发流程的有机组成部分,要求我们在软件开发的一开始,就要设计好相关的测试方法,从而让代码更加易于扩展、迭代和维护,实际上 Laravel 框架也是这么设计的,内置了一系列测试相关的 API 方便我们对代码进行测试。

单元测试 vs. 功能测试

说到测试,常见的测试主要包括单元测试和功能测试。

单元测试是一种通过编写测试代码来确认函数、类和方法是否以我们预期的方式来工作,单元测试会贯穿整个项目的开发周期。通过检查各个函数和方法的输入输出,就可以保证代码内部的逻辑已经正确执行,PHPUnit 就是最著名的单元测试框架。

功能测试是通过使用工具来生成自动化的测试用例,然后在真实的系统上运行,而不是单元测试中简单的验证单个模块的正确性。这些工具会使用有代表性的真实数据来模拟真实用户的行为从而验证系统的正确性,常见的测试工具有 Selenium,用于浏览器功能测试的 Laravel Dusk 就是基于 Selenium 实现的。

我们先来介绍单元测试。

单元测试的概念及意义

前面我们已经简单介绍了单元测试的概念,所谓「单元」指的就是代码区块,比如一个函数、一个类以及类的方法都是代码区块。单元测试这种软件测试方法就是用来检查指定的一个代码区块是否能够按照预期输出结果,单元测试一般通过编写独立的测试用例来完成,对应的流程图如下:

单元测试流程

首先,我们要编写测试用例对指定代码「单元」进行测试,然后运行测试用例看是否通过测试,如果通过继续编写下一个测试用例,不通过的话则修改「单元」代码后继续进行测试,如此往复直到所有「单元」代码测试通过。按照测试驱动开发的流程要求,需要先编写测试用例,再编写功能代码。

以上就是测试驱动开发的基本流程。这种开发模式的好处是可以确保所有代码问题必须在开发过程中被解决(当然,需要在代码测试覆盖率100%的前提下),因为一旦有代码不通过测试,则必须在问题修复之后才能进行后续代码编写,而且小的代码单元里定位问题也更加便捷。

有句话叫做「把可能出现的问题扼杀在摇篮里」,有效地防微杜渐,测试驱动开发可谓是充分可以达成这个目的,因为我们在开发过程中就已经把每一个小问题解决了,最终构建起来的完整系统也就没有问题了,反之,如果没有对代码做测试,让小问题不断堆积,最终会酿成大问题,在线上爆发,那就麻烦了。

PHPUnit 简介和安装配置

在 PHP 语言中,最著名的单元测试框架就是 PHPUnit 了,下面我们将以 PHPUnit 为例,演示如何在 PHP 项目中进行单元测试。

PHPUnit 目前有很多支持的版本,并且随着 PHP 版本的不同,功能也不尽相同,在选择版本时要注意与系统 PHP 版本的兼容性:

主版本 PHP版本兼容性 发布时间 支持期限
PHPUnit 8 PHP 7.2、7.3、7.4 2019年2月1日 2021年2月5日
PHPUnit 7 PHP 7.1、7.2、7.3 2018年2月2日 2020年2月7日
PHPUnit 6 PHP 7.0、7.1、7.2 2017年2月3日 2019年2月1日
PHPUnit 5 PHP 5.6、7.0、7.1 2015年10月2日 2018年2月2日

版本最新的是 PHPUnit 8,Laravel 5.8 目前底层默认使用的还是 PHPUnit 7,但你可以选择升级到 PHPUnit 8。关于各个版本的功能差异可以在官方文档上查看。

如果是在 Laravel 项目中,PHPUnit 已经开箱支持了,如果是在其它项目中使用,建议通过 Composer 进行安装:

composer require --dev phpunit/phpunit ^7

注:由于不会在线上环境进行测试,所以加上了 --dev 选项表示仅在本地安装。

这里我们以自己新建的空项目为例,首先通过 composer init 命令初始化 composer.json 文件,然后通过上述命令安装 PHPUnit。

当然,你也可以选择从官网下载最新版本或者你需要版本的 PHAR 文件到本地。

目录结构及初始化配置

安装完成后,项目的目录结构如下:

接下来,在项目目录下创建一个新的子目录来存放测试代码,仿照 Laravel 框架将其命名为 tests,接着在 tests 目录下创建子目录 Unit 用于存放单元测试代码。

然后在项目根目录下创建一个 phpunit.xml 文件用于编排 PHPUnit 测试,初始化该文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors = "true" verbose="true" stopOnFailure="false">

<testsuites>
	<testsuite name="Unit">
		<directory>./tests/Unit</directory>
	</testsuite>
</testsuites>
</phpunit>

其中 colors="true" 表示测试结果会高亮显示,<directory>./tests/Unit</directory> 则用于指定测试用例存放目录。

注:更多编排配置信息请参考 PHPUnit 官方文档

现在测试项目目录结构如下所示:

PHPUnit单元测试

接下来,我们就可以在 tests/Unit 目录下编写测试用例了。

编写第一个 PHPUnit 测试用例

单元测试基本约定

在编写测试用例之前,我们需要了解关于编写单元测试的一些常见约定:

  • 测试文件名需要以 Test 作为后缀,比如如果要测试 First.php,则对应的测试文件名为 FirstTest.php
  • 测试方法名需要以 test 作为前缀,比如如果要测试的方法名为 getuser,则对应的测试方法名为 testGetuser,此外,你还可以通过 @test 注解来声明一个测试方法;
  • 所有的测试方法可见性必须是 public
  • 所有的测试类都继承自 PHPUnit\Framework\TestCase

了解了这些约定之后,就可以编写第一个测试用例了。

编写简单测试用例

首先,我们来创建一个非常简单的测试用例,将其命名为 FirstTest.php,它不是对任何应用代码做测试,仅仅是为了写测试用例而编写:

然后我们就可以运行 phpunit 对刚刚编写的代码进行测试:

PHPUnit运行成功

通过编排文件 phpunit.xml,PHPUnit 会去 tests/Unit 目录中查找测试用例进行测试,测试通过则显示绿色的高亮文本,测试不通过则显示红色的警告文本,你也可以指定要测试的测试用例:

PHPUnit运行失败

这里一个最简单的测试用例,我们通过 TestCase 基类断言方法 assertTrue 判断指定变量是否为 true,如果是则测试通过,否则测试失败。

编写测试类方法的测试用例

接下来我们来编写一个稍微复杂一点的测试用例,通过这个测试用例来测试应用代码中的类方法。

在编写测试用例之前,先编写一个应用类 Email,我们在项目根目录下创建一个 app 目录,并将 Email.php 保存在该目录下,然后编写 Email 类代码如下:

<?php
declare(strict_types=1);

namespace App;

final class Email
{
   private $email;

   private function __construct(string $email)
   {
       $this->isValidEmail($email);
       $this->email = $email;
   }

   public static function fromString(string $email): self
   {
       return new self($email);
   }

   public function __toString(): string
   {
       return $this->email;
   }

   private function isValidEmail(string $email): void
   {
       if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
           throw new \InvalidArgumentException(
               sprintf(
                   '"%s" is not a valid email address',
                   $email
               )
           );
       }
   }
}

然后,我们在 composer.json 中配置 autoload 选项通过 PSR-4 规则加载 app 目录下的类文件:

"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
},

以便能够通过命名空间加载到 app 目录下的类。配置完成后,运行 composer dump-auto 命令重新生成自动加载文件以便 Email 类可以被正常加载到。

接下来,我们在 tests/Unit 目录下创建一个新的测试用例 EmailTest.php,用来测试刚刚编写的 Email 类:

<?php
namespace Unit\Test;
use PHPUnit\Framework\TestCase;
use App\Email;

class EmailTest extends TestCase
{
	public function testCanBeCreatedFromValidEmailAddress(): void
    {
       $this->assertInstanceOf(
           Email::class,
           Email::fromString('user@example.com')
       );
   }

   public function testCannotBeCreatedFromInvalidEmailAddress(): void
   {
       $this->expectException(\InvalidArgumentException::class);
       Email::fromString('invalid');
   }

   public function testCanBeUsedAsString(): void
   {
       $this->assertEquals(
           'user@example.com',
           Email::fromString('user@example.com')
       );
   }
}

在上述代码中,第一个测试方法使用了基类提供的断言方法 assertInstanceOf 判断 Email::fromString 方法返回的是否是 Email 实例;第二个测试方法中使用了 expectException 判断传入无效的邮件地址是否抛出指定异常;第三个测试方法则使用 assertEquals 判断 Email::fromString 打印结果是否与给定字符串相等。

下面我们来运行单元测试:

所有测试用例都显示绿色(包含了 FirstTest 里面的那个用例,所以是 4 个),表明 Email 类方法中的代码是按照我们预期的结果执行,测试通过。

好了,关于基于 PHPUnit 编写测试用例我们先简单介绍到这里,更多使用方法请参考官方文档,从下一篇开始我们将介绍如何在 Laravel 框架中集成 PHPUnit 编写单元测试代码。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: Laravel 框架如何基于 Composer 实现类和文件的自动加载

>> 下一篇: 在 Laravel 中基于 PHPUnit 进行代码测试:目录结构及测试编排文件 phpunit.xml 详解