简介
Laravel 植根于测试,实际上,内置使 PHPUnit 对测试提供支持是开箱即用的,并且 phpunit.xml
文件已经为应用设置好了。框架还提供了方便的辅助方法允许你对应用进行优雅的测试。
默认情况下,tests
目录包含了两个子目录:Feature
和 Unit
,分别用于功能测试和单元测试,单元测试专注于小的、相互隔离的代码,实际上,大部分单元测试可能都是聚焦于单个方法。功能测试可用于测试较大区块的代码,包括若干组件之前的交互,甚至一个完整的HTTP请求。
单元测试 & 功能测试
说到测试,常见的测试主要包括单元测试和功能测试。
单元测试是一种通过编写测试代码来确认函数、类和方法是否以我们预期的方式来工作,单元测试会贯穿整个项目的开发周期。通过检查各个函数和方法的输入输出,就可以保证代码内部的逻辑已经正确执行,PHPUnit就是最著名的单元测试框架。
功能测试是通过使用工具来生成自动化的测试用例,然后在真实的系统上运行,而不是单元测试中简单的验证单个模块的正确性。这些工具会使用有代表性的真实数据来模拟真实用户的行为从而验证系统的正确性,常见的测试工具有Selenium ,用于浏览器功能测试的 Laravel Dusk 就是基于 Selenium 实现的。
总结一句话:
单元测试:站在程序员的角度从内部测试应用,去检查代码是否合理清晰明确。
功能测试:站在用户的角度,从外部测试应用,查看是否能到达效果。
单元测试基本约定
在编写测试用例之前,先了解一下编写单元测试的一些常见的约定:
- 测试文件名需要以 Test 作为后缀,比如要测试First.php ,则对应的测试文件名为 FirstTest.php;
- 测试方法需要以 test 作为前缀,比如要测试的方法名为login, 则对应的方法名为 testLogin;
- 所有的测试方法可见性必须是 public;
- 所有的测试类都继承自 PHPUnit\Framework\TestCase。
测试编排文件 phpunit.xml 详解
在 Laravel 项目根目录下有一个 phpunit.xml 文件,是 PHPUnit 的编排文件,用于编排和初始化 PHPUnit 的测试行为,PHPUnit 在执行测试之前会基于这个文件进行初始化设置,你可以将其看作是 PHPUnit 的配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<server name="APP_ENV" value="testing"/>
<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>
</phpunit>
该文件的第一行是 XML 文件的版本和编码描述信息,从第二行开始的 <phpunit> 元素则正式开始配置 PHPUnit 的核心功能,在该元素里面还嵌套定义了其它子元素,用于配置测试套件、过滤器、PHP 变量等其它信息。
通用配置
首先来看 phpunit
元素上的属性,其中很多属性其实都可以在执行 phpunit
命令时通过命令行参数的形式传入,但是如果参数太多,且每次传入参数都是一样的,显然配置到 phpunit.xml
中更方便,也更加易于维护,PHPUnit 执行的命令行参数可以在这里查看,或者通过 phpunit --help
在命令行查看:
backupGlobals
属性对应命令行参数里的--globals-backup
,用于在每个测试中备份和恢复 PHP 超全局变量$GLOBALS
,这里设置为false
表示不做相应的备份和恢复操作。backupStaticAttributes
属性对应命令行参数里的static-backup
,用于在每个测试中备份和恢复静态属性,这里设置为false
表示不做相应的备份和恢复操作。bootstrap
属性对应命令行参数里面的--bootstrap <file>
,用于指定测试运行前需要引入的文件,这里配置为vendor/autoload.php
表示会引入 Composer 自动加载和管理的所有依赖,以便在测试文件中使用。colors
属性对应命令行参数里的--colors=<flag>
,用于指示在输出中是否用颜色进行标识。processIsolation
属性对应命令行参数里的--process-isolation
,用于表示是否在隔离的 PHP 进程中执行测试。stopOnFailure
属性对应命令行参数里的--stop-on-failure
,用于表示测试出错或失败时是否退出脚本执行,配置为false
表示不退出。
接下来是一些不能通过命令行参数指定的属性:
convertErrorsToExceptions
属性用于定义是否将 PHP ERROR 级别错误转化为异常,默认会转化为异常的错误类型包括:E_WARNING
、E_NOTICE
、E_USER_ERROR
、E_USER_WARNING
、E_USER_NOTICE
、E_STRICT
、E_RECOVERABLE_ERROR
、E_DEPRECATED
、E_USER_DEPRECATED
,这里将该属性设置为true
表示启用该功能。convertNoticesToExceptions
属性用于定义是否将 PHP NOTICE 级别错误转化为异常,设置为true
表示会将E_NOTICE
、E_USER_NOTICE
、E_STRICT
三种级别错误转化为异常。convertWarningsToExceptions
属性用于定义是否将 PHP WARNING 级别错误转化为异常,设置为true
表示会将E_WARNING
或E_USER_WARNING
级别错误转化为异常。
当然,这里只包含了 PHPUnit 所支持的 phpunit
配置的一部分属性,更多配置请参考官方文档 及 PHPUnit 命令行参数配置。
测试套件
Laravel 框架默认配置的测试套件定义在子元素 <testsuites>
中,默认通过 <testsuites>
定义了两个 <testsuite>
,分别是用于单元测试的 Unit
和用于功能的测试的 Feature
,在它们各自的测试套件中,通过 directory
子元素指定对应测试文件所在的目录,并通过 suffix
属性指定测试文件的文件名后缀,这样,当运行 phpunit
命令时,PHPUnit 会从指定目录下匹配指定后缀的测试文件进行测试。
在运行 phpunit
命令时,我们可以通过相应测试套件的名称匹配要执行的测试用例:
vendor/bin/phpunit --testsuite=Unit
更多测试套件的配置选项可以参考官方文档。
过滤器
另外,Laravel 框架还通过 <filter>
元素配置了过滤器,在该元素中我们可以通过 whitelist
子元素指定用于配置代码覆盖率报告分析所使用的白名单,代码覆盖率是代码测试中一个很重要的概念,我们的测试代码要尽可能覆盖到 100% 的业务代码,这样的测试才有意义,而 Laravel 应用代码都位于项目根目录下的 app
目录中,并且我们只测试 PHP 代码,所以在 <whitelist>
中通过 directory
子元素做了相应的配置。
这样,我们在运行 phpunit
时加上 --coverage-html .
参数,就可以在根目录下生成 HTML 格式的测试覆盖率报告文档了。
PHP 变量
最后,Laravel 框架还通过 <php>
元素为我们初始化了一些 PHPUnit 测试环境下的 PHP 常量:
<php>
<server name="APP_ENV" value="testing"/>
<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>
上述配置相当于以下PHP代码:
$_SERVER['APP_ENV'] = 'testing';
$_SERVER['BCRYPT_ROUNDS'] = '4';
$_SERVER['CACHE_DRIVER'] = 'array';
$_SERVER['MAIL_DRIVER'] = 'array';
$_SERVER['QUEUE_CONNECTION'] = 'sync';
$_SERVER['SESSION_DRIVER'] = 'array';
在 Laravel 测试环境下,APP_ENV
的值是 testing,
缓存、邮件、会话驱动都是通过数组模拟,因而不会持久化到硬盘,此外队列驱动是 sync
,表示会同步执行推送到队列的任务。
除此之外,还可以初始化 PHP 请求、常量、INI 设置、Cookie、超全局变量等信息,更多使用明细请参考官方文档。
HTTP测试
测试用户认证
在 Laravel 中,可以通过框架提供的一系列断言方法对用户认证状态进行测试。
编写用户注册登录相关的测试用例,对用户注册、登录功能进行测试,由于涉及到多个测试用例,我们编写一个新的功能测试类:
php artisan make:test AuthTest
该命令会在 test/Feature 目录下创建一个 AuthTest 类,我们在这个测试类中编写一系列测试用例如下:
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class AuthTest extends TestCase
{
use DatabaseTransactions; // 防止数据污染
/**
* A basic test example.
*
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
/**
* Desc: 测试登陆表单页
* Author: chenxl
* DateTime: 2021/5/7 16:18
*/
public function testLoginForm()
{
$response = $this->get('/admin/login');
$response->assertSuccessful(); // 断言响应状态码是否介于200-300之间
$response->assertStatus(200); // 断言响应状态码是200
}
/**
* Desc: 测试用户提交登陆表单
* Author: chenxl
* DateTime: 2021/5/7 16:21
*/
public function testPostLogin()
{
$admin = Admin::where('username', 'admin')->first();
$response = $this->post('/admin/check', [
'username' => $admin->username,
'password' => 'password',
]);
//dd($response->content());
// $user = auth('admin')->user();
// dd($user->username);
$response->assertRedirect('/'); // 用户登录成功后跳转到 /home
}
/**
* Desc: 测试未认证状态下访问 / 路由
* Author: chenxl
* DateTime: 2021/5/7 16:29
*/
public function testHomeWithoutAuthenticated()
{
$response = $this->get('/');
$response->assertRedirect('/admin/login'); // 用户未认证则跳转到登录页
$this->assertGuest(); // 断言用户未认证
// 断言给定认证凭证是否与数据库记录匹配
$this->assertCredentials([
'username' => 'admin',
'password' => 'password'
], 'admin');
}
/**
* Desc: 测试模拟用户认证状态下访问 / 路由
* Author: chenxl
* DateTime: 2021/5/7 16:56
*/
public function testHomeWithAuthenticated()
{
$admin = new User([
'id' => 10000,
'name' => 'abc'
]);
$this->actingAs($admin, 'admin')->get('/'); //使用 actionAs 方法进行模拟认证时,可以通过以下方法断言用户认证:
$this->assertAuthenticatedAs($admin, 'admin'); // 断言用户认证且以 $admin 身份认证
$this->assertAuthenticated('admin'); //断言用户是否认证,默认参数 guard=web
}
/**
* Desc: 真实用户登录,表单提交,DatabaseTransactions防止数据有污染数据
* Author: chenxl
* DateTime 2021/4/30 10:41
*/
public function testAddYard()
{
$admin = Admin::where('username', 'admin')->first();
// dd($admin->toArray());
$response = $this->post('/admin/check', [
'username' => $admin->username,
'password' => 'password',
]);
//dd($response->content());
$response->assertRedirect('/'); // 用户登录成功后跳转到 /home
$res = $this->post('/admin/yard/add', [
'course_ids' => '1001,1002,1003',
'city_id' => 3,
'city_name' => '石家庄',
]);
// dd($res->content());
$res->assertStatus(200);
$response->assertRedirect('/');
}
}
在认证用户时,我们可以通过模拟提交表单请求或者直接通过 Laravel 提供的 actionAs
方法,使用 actionAs
方法进行模拟认证时,可以通过以下方法断言用户认证:
assertAuthenticated
:断言用户是否认证,默认参数为guard=web
,你可以指定其他的guard
,与之相对的断言方法是assertGuest
,同样,你可以传参指定认证guard
;assertAuthenticatedAs
:断言用户是否以指定用户实例认证,你可以通过第二个参数指定相应的guard
参数;
此外,还可以通过 assertCredentials
方法断言给定认证凭证是否与数据库记录匹配。
在表单提交需要添加或修改数据库时,use DatabaseTransactions防止数据污染。
我们在命令行中运行 phpunit
运行上述测试用例:
./vendor/bin/phpunit tests/Feature/AuthTest.php
运行结果如下,测试通过:
测试文件上传
测试文件上传的时候,不需要真的上传一个文件,因为这个操作可能会非常耗时,取而代之地,我们可以通过 Illuminate\Http\UploadedFile
类提供的 fake
方法「伪造」文件用于测试,此外,Storage
门面也提供了 fake
方法用于「伪造」文件目录,结合这两个伪造功能,我们可以在 Laravel 框架中轻松实现文件上传测试:
public function testFileUpload()
{
Storage::fake('photos'); // 伪造目录
$photo = UploadedFile::fake()->image('picture.jpg'); // 伪造上传图片
// 自定义伪造图片宽、高,大小
// $photo = UploadedFile::fake()->image('picture.jpg', 100, 100)->size(100);
// 可以使用 create 方法创建其他类型的文件
// $file = UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);
$this->post('/photo', [
'photo' => $photo
]);
Storage::disk('photos')->assertMissing('picture.jpg'); // 断言文件是否上传成功
}
需要注意的是,通过 Storage::fake
方法伪造的 photos
目录位于 storage/framework/testing/disks/photos
而不是 storage/app/photos
,所以上述测试用例会通过。与之相对的,如果要断言指定文件存在,可以使用 Storage::disk('photos')->assertExists
方法。
测试 JSON API
HTTP 响应除了返回 Web 视图之外,还可以返回 API 接口,而目前 API 接口的数据格式又以 JSON 为主,下面我们就以 JSON API 为例演示如何对 API 接口编写测试用例。
我们在 routes/api.php 中定义一个测试路由 /api/test
Route::get('test/{id}', function ($id) {
$user = \App\User::find($id);
return [
'site' => 'Laravel学院',
'creator' => '学院君',
'user' => $user
];
});
Laravel 框架为所有 JSON API 请求方法提供了支持,你可以通过 json
方法的第一个参数来指定 HTTP 请求方法,比如 GET
、POST
、PUT
、DELETE
等,也可以通过形如 getJson
、postJson
、putJson
、deleteJson
这些方法,这样,就不需要传递请求方法参数了。
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ApiTest extends TestCase
{
/**
* 测试 JSON API 响应
*
* @return void
*/
public function testJsonApi()
{
$response = $this->json('GET', '/api/test/1');
// $response = $this->json('GET', '/api/test/1');
// $response = $this->getjson('/api/test/1');
// dd($response->content());
$response->assertStatus(200)
->assertJson([
'site' => 'Laravel学院',
'creator' => '学院君'
])
->assertJsonMissing([
'name' => '测试用户'
])
->assertJsonCount(6, 'user')
->assertJsonFragment([
'site' => 'Laravel学院'
]);
}
}
在这个测试用例中,我们首先通过 assertStatus
方法断言接口响应状态码是否是200,然后通过 assertJson
方法断言返回 JSON 数据中是否包含给定数据,与之相对的的是 assertJsonMissing
方法,用于断言返回 JSON 数据中不包含给定键。然后,通过 assertJsonCount
方法断言给定键值下数据项的个数,以及 assertJsonFragment
方法断言 JSON 数据中是否包含给定片段,该方法和 assertJson
方法类似。
运行上述测试用例:
./vendor/bin/phpunit tests/Feature/ApiTest.php
结果如下:
除此之外,Laravel 框架还提供了以下断言方法对 JSON 响应数据进行测试:
assertJsonStructure
:断言返回 JSON 响应是否包含给定的数据结构;assertExactJson
:断言返回 JSON 响应是否与期望数据完全一致;assertJsonMissingExact
:断言返回 JSON 响应是否包含给定的完整 JSON 片段;assertJsonValidationErrors
:断言 JSON 响应包含给定键对应的 JSON 格式验证错误信息;assertJsonMissingValidationErrors
:与assertJsonValidationErrors
方法相对。
测试视图模板
Laravel 框架还提供了对视图模板进行测试的断言方法,以首页 /
路由为例:
Route::get('/', function () {
return view('welcome', ['website' => 'Laravel', 'author' => '学院君']);
});
其对应的视图名称是 welcome
,对应视图模板文件位于 resources/views/welcome.blade.php
,接下来,我们来编写一个测试用例测试该视图:
public function testView()
{
$response = $this->get('/');
$response->assertSeeText('Laravel') // 断言响应实体中是否包含给定字符串
->assertViewHas('website', 'Laravel')
->assertViewMissing('name')
->assertViewHasAll(['website', 'author' => '学院君'])
->assertViewIs('welcome');
}
我们可以通过 assertViewHas
方法断言传递给视图的变量中是否包含某个变量,也可以通过 assertViewHasAll
方法一次性断言多个传递给视图的变量,变量值可以指定,也可以不指定;与之相对的,还可以通过 assertViewMissing
方法断言传递给视图的变量中不包含某个变量。
最后,还可以通过 assertViewIs
方法断言视图名称是否与指定值匹配,该名称即控制器或路由闭包中传递到 view
方法的第一个参数值。
参考: https://laravelacademy.org/books/laravel-tutorial/chapter/test-driven-development