测试
介绍
Laravel 是以测试为核心构建的。实际上,支持使用 PHPUnit 进行测试是开箱即用的,并且已经为您的应用程序设置了一个 phpunit.xml
文件。该框架还提供了方便的辅助方法,使您能够更具表现力地测试您的应用程序。
在 tests
目录中提供了一个 ExampleTest.php
文件。安装新的 Laravel 应用程序后,只需在命令行中运行 phpunit
即可运行您的测试。
测试环境
运行测试时,Laravel 会自动将配置环境设置为 testing
。Laravel 在测试时自动将会话和缓存配置为 array
驱动,这意味着在测试期间不会持久化任何会话或缓存数据。
您可以根据需要创建其他测试环境配置。可以在 phpunit.xml
文件中配置 testing
环境变量,但在运行测试之前,请确保使用 config:clear
Artisan 命令清除配置缓存!
定义和运行测试
要创建新的测试用例,请使用 make:test
Artisan 命令:
php artisan make:test UserTest
此命令将在您的 tests
目录中放置一个新的 UserTest
类。然后,您可以像使用 PHPUnit 一样定义测试方法。要运行测试,只需从终端执行 phpunit
命令:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class UserTest extends TestCase
{
/**
* 一个基本的测试示例。
*
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
}
如果您在测试类中定义了自己的 setUp
方法,请确保调用 parent::setUp
。
应用程序测试
Laravel 提供了一个非常流畅的 API,用于向您的应用程序发出 HTTP 请求、检查输出,甚至填写表单。例如,请查看 tests
目录中包含的 ExampleTest.php
文件:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5')
->dontSee('Rails');
}
}
visit
方法向应用程序发出 GET
请求。see
方法断言我们应该在应用程序返回的响应中看到给定的文本。dontSee
方法断言给定的文本未在应用程序响应中返回。这是 Laravel 中最基本的应用程序测试。
与应用程序交互
当然,您可以做的不仅仅是断言给定响应中出现的文本。让我们看看一些点击链接和填写表单的示例:
点击链接
在此测试中,我们将向应用程序发出请求,在返回的响应中“点击”一个链接,然后断言我们到达了给定的 URI。例如,假设我们的响应中有一个文本值为“关于我们”的链接:
<a href="/about-us">关于我们</a>
现在,让我们编写一个测试来点击链接并断言用户到达正确的页面:
public function testBasicExample()
{
$this->visit('/')
->click('关于我们')
->seePageIs('/about-us');
}
处理表单
Laravel 还提供了几种方法来测试表单。type
、select
、check
、attach
和 press
方法允许您与表单的所有输入进行交互。例如,假设此表单存在于应用程序的注册页面上:
<form action="/register" method="POST">
{{ csrf_field() }}
<div>
姓名:<input type="text" name="name">
</div>
<div>
<input type="checkbox" value="yes" name="terms"> 接受条款
</div>
<div>
<input type="submit" value="注册">
</div>
</form>
我们可以编写一个测试来完成此表单并检查结果:
public function testNewUserRegistration()
{
$this->visit('/register')
->type('Taylor', 'name')
->check('terms')
->press('注册')
->seePageIs('/dashboard');
}
当然,如果您的表单包含其他输入,例如单选按钮或下拉框,您也可以轻松填写这些类型的字段。以下是每种表单操作方法的列表:
方法 | 描述 |
---|---|
$this->type($text, $elementName) | 在给定字段中“输入”文本。 |
$this->select($value, $elementName) | “选择”单选按钮或下拉字段。 |
$this->check($elementName) | “选中”复选框字段。 |
$this->uncheck($elementName) | “取消选中”复选框字段。 |
$this->attach($pathToFile, $elementName) | 将文件“附加”到表单。 |
$this->press($buttonTextOrElementName) | 按下具有给定文本或名称的按钮。 |
处理附件
如果您的表单包含 file
输入类型,您可以使用 attach
方法将文件附加到表单:
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->type('文件名', 'name')
->attach($absolutePathToFile, 'photo')
->press('上传')
->see('上传成功!');
}
测试 JSON API
Laravel 还提供了几种用于测试 JSON API 及其响应的辅助工具。例如,可以使用 get
、post
、put
、patch
和 delete
方法发出带有各种 HTTP 动词的请求。您还可以轻松地将数据和头信息传递给这些方法。首先,让我们编写一个测试来向 /user
发出 POST
请求,并断言以 JSON 格式返回给定数组:
<?php
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->json('POST', '/user', ['name' => 'Sally'])
->seeJson([
'created' => true,
]);
}
}
seeJson
方法将给定数组转换为 JSON,然后验证 JSON 片段是否在应用程序返回的整个 JSON 响应中任何地方出现。因此,如果 JSON 响应中有其他属性,只要给定片段存在,此测试仍将通过。
验证精确的 JSON 匹配
如果您想验证给定数组与应用程序返回的 JSON 是精确匹配的,您应该使用 seeJsonEquals
方法:
<?php
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->json('POST', '/user', ['name' => 'Sally'])
->seeJsonEquals([
'created' => true,
]);
}
}
验证结构性 JSON 匹配
还可以验证 JSON 响应是否符合特定结构。为此,您应该使用 seeJsonStructure
方法并传递一个(嵌套的)键列表:
<?php
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->get('/user/1')
->seeJsonStructure([
'name',
'pet' => [
'name', 'age'
]
]);
}
}
上面的示例说明了期望接收一个 name
和一个嵌套的 pet
对象,其中包含自己的 name
和 age
。如果响应中存在其他键,seeJsonStructure
不会失败。例如,如果 pet
具有 weight
属性,测试仍将通过。
您可以使用 *
断言返回的 JSON 结构中有一个列表,其中每个列表项至少包含在值集中找到的属性:
<?php
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
// 断言列表中的每个用户至少具有 id、name 和 email 属性。
$this->get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email'
]
]);
}
}
您还可以嵌套 *
符号。在这种情况下,我们将断言 JSON 响应中的每个用户包含一组给定的属性,并且每个用户的每个宠物也包含一组给定的属性:
$this->get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email', 'pets' => [
'*' => [
'name', 'age'
]
]
]
]);
会话 / 认证
Laravel 提供了几种用于在测试期间处理会话的辅助工具。首先,您可以使用 withSession
方法将会话数据设置为给定数组。这对于在测试请求应用程序之前加载会话数据非常有用:
<?php
class ExampleTest extends TestCase
{
public function testApplication()
{
$this->withSession(['foo' => 'bar'])
->visit('/');
}
}
当然,会话的一个常见用途是维护用户状态,例如已认证的用户。actingAs
辅助方法提供了一种简单的方法来将给定用户认证为当前用户。例如,我们可以使用模型工厂生成并认证用户:
<?php
class ExampleTest extends TestCase
{
public function testApplication()
{
$user = factory(App\User::class)->create();
$this->actingAs($user)
->withSession(['foo' => 'bar'])
->visit('/')
->see('Hello, '.$user->name);
}
}
您还可以通过将守卫名称作为第二个参数传递给 actingAs
方法来指定应使用哪个守卫来认证给定用户:
$this->actingAs($user, 'backend')
禁用中间件
在测试应用程序时,您可能会发现禁用某些测试的中间件很方便。这将允许您在不考虑中间件的情况下测试路由和控制器。Laravel 包含一个简单的 WithoutMiddleware
trait,您可以使用它来自动禁用测试类的所有中间件:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use WithoutMiddleware;
//
}
如果您只想为少数测试方法禁用中间件,可以在测试方法中调用 withoutMiddleware
方法:
<?php
class ExampleTest extends TestCase
{
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->withoutMiddleware();
$this->visit('/')
->see('Laravel 5');
}
}
自定义 HTTP 请求
如果您想向应用程序发出自定义 HTTP 请求并获取完整的 Illuminate\Http\Response
对象,可以使用 call
方法:
public function testApplication()
{
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
}
如果您正在进行 POST
、PUT
或 PATCH
请求,可以在请求中传递一个输入数据数组。当然,这些数据将在您的路由和控制器中通过请求实例可用:
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
PHPUnit 断言
Laravel 为 PHPUnit 测试提供了几种额外的断言方法:
方法 | 描述 |
---|---|
->assertResponseOk(); | 断言客户端响应具有 OK 状态码。 |
->assertResponseStatus($code); | 断言客户端响应具有给定代码。 |
->assertViewHas($key, $value = null); | 断言响应视图具有给定的绑定数据。 |
->assertViewHasAll(array $bindings); | 断言视图具有给定的绑定数据列表。 |
->assertViewMissing($key); | 断言响应视图缺少给定的绑定数据。 |
->assertRedirectedTo($uri, $with = []); | 断言客户端是否重定向到给定 URI。 |
->assertRedirectedToRoute($name, $parameters = [], $with = []); | 断言客户端是否重定向到给定路由。 |
->assertRedirectedToAction($name, $parameters = [], $with = []); | 断言客户端是否重定向到给定操作。 |
->assertSessionHas($key, $value = null); | 断言会话具有给定值。 |
->assertSessionHasAll(array $bindings); | 断言会话具有给定值列表。 |
->assertSessionHasErrors($bindings = [], $format = null); | 断言会话绑定了错误。 |
->assertHasOldInput(); | 断言会话具有旧输入。 |
->assertSessionMissing($key); | 断言会话缺少给定键。 |
与数据库交互
Laravel 还提供了多种有用的工具,使测试数据库驱动的应用程序变得更容易。首先,您可以使用 seeInDatabase
辅助工具断言数据库中存在与给定条件匹配的数据。例如,如果我们想验证 users
表中是否有一条 email
值为 sally@example.com
的记录,我们可以这样做:
public function testDatabase()
{
// 调用应用程序...
$this->seeInDatabase('users', ['email' => 'sally@example.com']);
}
当然,seeInDatabase
方法和其他类似的辅助工具是为了方便。您可以自由使用 PHPUnit 的任何内置断言方法来补充您的测试。
在每次测试后重置数据库
在每次测试后重置数据库通常很有用,以便前一个测试的数据不会干扰后续测试。
使用迁移
一种选择是在每次测试后回滚数据库,并在下一个测试之前迁移它。Laravel 提供了一个简单的 DatabaseMigrations
trait,可以自动为您处理此问题。只需在测试类上使用该 trait:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseMigrations;
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5');
}
}
使用事务
另一种选择是将每个测试用例包装在数据库事务中。同样,Laravel 提供了一个方便的 DatabaseTransactions
trait,可以自动处理此问题:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseTransactions;
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5');
}
}
此 trait 仅将默认数据库连接包装在事务中。
模型工厂
在测试时,通常需要在执行测试之前向数据库插入一些记录。与其在创建此测试数据时手动指定每个列的值,Laravel 允许您使用“工厂”为每个 Eloquent 模型定义一组默认属性。要开始,请查看应用程序中的 database/factories/ModelFactory.php
文件。开箱即用,此文件包含一个工厂定义:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
在作为工厂定义的闭包中,您可以返回模型上所有属性的默认测试值。闭包将接收一个 Faker PHP 库的实例,该库允许您方便地生成各种类型的随机数据以进行测试。
当然,您可以自由地将自己的其他工厂添加到 ModelFactory.php
文件中。您还可以为每个模型创建额外的工厂文件以便更好地组织。例如,您可以在 database/factories
目录中创建 UserFactory.php
和 CommentFactory.php
文件。
多种工厂类型
有时,您可能希望为同一个 Eloquent 模型类拥有多个工厂。例如,您可能希望除了普通用户之外,还拥有“管理员”用户的工厂。您可以使用 defineAs
方法定义这些工厂:
$factory->defineAs(App\User::class, 'admin', function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
'admin' => true,
];
});
与其复制基本用户工厂的所有属性,不如使用 raw
方法检索基本属性。一旦拥有了属性,只需补充任何所需的额外值:
$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
$user = $factory->raw(App\User::class);
return array_merge($user, ['admin' => true]);
});
在测试中使用工厂
一旦定义了工厂,您可以在测试或数据库种子文件中使用它们来生成模型实例,使用全局 factory
函数。因此,让我们看看创建模型的一些示例。首先,我们将使用 make
方法,该方法创建模型但不将其保存到数据库:
public function testDatabase()
{
$user = factory(App\User::class)->make();
// 在测试中使用模型...
}
如果您想覆盖模型的某些默认值,可以将值数组传递给 make
方法。只有指定的值会被替换,而其余的值将保持为工厂指定的默认值:
$user = factory(App\User::class)->make([
'name' => 'Abigail',
]);
您还可以创建多个模型的集合或创建给定类型的模型:
// 创建三个 App\User 实例...
$users = factory(App\User::class, 3)->make();
// 创建一个 App\User "admin" 实例...
$user = factory(App\User::class, 'admin')->make();
// 创建三个 App\User "admin" 实例...
$users = factory(App\User::class, 'admin', 3)->make();
持久化工厂模型
create
方法不仅创建模型实例,还使用 Eloquent 的 save
方法将它们保存到数据库:
public function testDatabase()
{
$user = factory(App\User::class)->create();
// 在测试中使用模型...
}
同样,您可以通过将数组传递给 create
方法来覆盖模型上的属性:
$user = factory(App\User::class)->create([
'name' => 'Abigail',
]);
向模型添加关系
您甚至可以将多个模型持久化到数据库中。在此示例中,我们甚至会将关系附加到创建的模型上。使用 create
方法创建多个模型时,将返回一个 Eloquent 集合实例,允许您使用集合提供的任何方便函数,例如 each
:
$users = factory(App\User::class, 3)
->create()
->each(function ($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
关系和属性闭包
您还可以使用工厂定义中的闭包属性将关系附加到模型上。例如,如果您想在创建 Post
时创建一个新的 User
实例,可以执行以下操作:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
这些闭包还会接收包含它们的工厂的评估属性数组:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
}
];
});
模拟
模拟事件
如果您大量使用 Laravel 的事件系统,您可能希望在测试时静音或模拟某些事件。例如,如果您正在测试用户注册,您可能不希望所有 UserRegistered
事件的处理程序触发,因为这些可能会发送“欢迎”电子邮件等。
Laravel 提供了一个方便的 expectsEvents
方法,可以验证预期事件是否被触发,但阻止这些事件的任何处理程序运行:
<?php
class ExampleTest extends TestCase
{
public function testUserRegistration()
{
$this->expectsEvents(App\Events\UserRegistered::class);
// 测试用户注册...
}
}
您可以使用 doesntExpectEvents
方法验证给定事件未触发:
<?php
class ExampleTest extends TestCase
{
public function testPodcastPurchase()
{
$this->expectsEvents(App\Events\PodcastWasPurchased::class);
$this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);
// 测试购买播客...
}
}
如果您想阻止所有事件处理程序运行,可以使用 withoutEvents
方法:
<?php
class ExampleTest extends TestCase
{
public function testUserRegistration()
{
$this->withoutEvents();
// 测试用户注册代码...
}
}
模拟任务
有时,您可能只想测试在向应用程序发出请求时控制器是否调度了特定任务。这允许您在隔离的情况下测试路由/控制器 - 与任务的逻辑分开。当然,您可以在单独的测试类中测试任务本身。
Laravel 提供了一个方便的 expectsJobs
方法,可以验证预期任务是否被调度,但任务本身不会被执行:
<?php
class ExampleTest extends TestCase
{
public function testPurchasePodcast()
{
$this->expectsJobs(App\Jobs\PurchasePodcast::class);
// 测试购买播客代码...
}
}
此方法仅检测通过 DispatchesJobs
trait 的调度方法或 dispatch
辅助函数调度的任务。它不检测直接发送到 Queue::push
的任务。
模拟 Facades
在测试时,您可能经常想要模拟对 Laravel facade 的调用。例如,考虑以下控制器操作:
<?php
namespace App\Http\Controllers;
use Cache;
class UserController extends Controller
{
/**
* 显示应用程序的所有用户列表。
*
* @return Response
*/
public function index()
{
$value = Cache::get('key');
//
}
}
我们可以通过使用 shouldReceive
方法来模拟对 Cache
facade 的调用,该方法将返回一个 Mockery 模拟的实例。由于 facades 实际上是由 Laravel 服务容器 解析和管理的,因此它们比典型的静态类具有更高的可测试性。例如,让我们模拟对 Cache
facade 的调用:
<?php
class FooTest extends TestCase
{
public function testGetIndex()
{
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$this->visit('/users')->see('value');
}
}
您不应模拟 Request
facade。相反,在运行测试时,将所需的输入传递给 HTTP 辅助方法,如 call
和 post
。