Skip to content

测试

介绍

Laravel 是以测试为核心构建的。实际上,支持使用 PHPUnit 进行测试是开箱即用的,并且已经为您的应用程序设置了一个 phpunit.xml 文件。该框架还提供了方便的辅助方法,使您能够更具表现力地测试您的应用程序。

tests 目录中提供了一个 ExampleTest.php 文件。安装新的 Laravel 应用程序后,只需在命令行中运行 phpunit 即可运行您的测试。

测试环境

运行测试时,Laravel 会自动将配置环境设置为 testing。Laravel 在测试时自动将会话和缓存配置为 array 驱动,这意味着在测试期间不会持久化任何会话或缓存数据。

您可以根据需要创建其他测试环境配置。可以在 phpunit.xml 文件中配置 testing 环境变量,但在运行测试之前,请确保使用 config:clear Artisan 命令清除配置缓存!

定义和运行测试

要创建新的测试用例,请使用 make:test Artisan 命令:

php
php artisan make:test UserTest

此命令将在您的 tests 目录中放置一个新的 UserTest 类。然后,您可以像使用 PHPUnit 一样定义测试方法。要运行测试,只需从终端执行 phpunit 命令:

php
<?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);
    }
}
lightbulb

如果您在测试类中定义了自己的 setUp 方法,请确保调用 parent::setUp

应用程序测试

Laravel 提供了一个非常流畅的 API,用于向您的应用程序发出 HTTP 请求、检查输出,甚至填写表单。例如,请查看 tests 目录中包含的 ExampleTest.php 文件:

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。例如,假设我们的响应中有一个文本值为“关于我们”的链接:

html
<a href="/about-us">关于我们</a>

现在,让我们编写一个测试来点击链接并断言用户到达正确的页面:

php
public function testBasicExample()
{
    $this->visit('/')
         ->click('关于我们')
         ->seePageIs('/about-us');
}

处理表单

Laravel 还提供了几种方法来测试表单。typeselectcheckattachpress 方法允许您与表单的所有输入进行交互。例如,假设此表单存在于应用程序的注册页面上:

html
<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>

我们可以编写一个测试来完成此表单并检查结果:

php
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 方法将文件附加到表单:

php
public function testPhotoCanBeUploaded()
{
    $this->visit('/upload')
         ->type('文件名', 'name')
         ->attach($absolutePathToFile, 'photo')
         ->press('上传')
         ->see('上传成功!');
}

测试 JSON API

Laravel 还提供了几种用于测试 JSON API 及其响应的辅助工具。例如,可以使用 getpostputpatchdelete 方法发出带有各种 HTTP 动词的请求。您还可以轻松地将数据和头信息传递给这些方法。首先,让我们编写一个测试来向 /user 发出 POST 请求,并断言以 JSON 格式返回给定数组:

php
<?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
<?php

class ExampleTest extends TestCase
{
    /**
     * 一个基本的功能测试示例。
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->json('POST', '/user', ['name' => 'Sally'])
             ->seeJsonEquals([
                 'created' => true,
             ]);
    }
}

验证结构性 JSON 匹配

还可以验证 JSON 响应是否符合特定结构。为此,您应该使用 seeJsonStructure 方法并传递一个(嵌套的)键列表:

php
<?php

class ExampleTest extends TestCase
{
    /**
     * 一个基本的功能测试示例。
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->get('/user/1')
             ->seeJsonStructure([
                 'name',
                 'pet' => [
                     'name', 'age'
                 ]
             ]);
    }
}

上面的示例说明了期望接收一个 name 和一个嵌套的 pet 对象,其中包含自己的 nameage。如果响应中存在其他键,seeJsonStructure 不会失败。例如,如果 pet 具有 weight 属性,测试仍将通过。

您可以使用 * 断言返回的 JSON 结构中有一个列表,其中每个列表项至少包含在值集中找到的属性:

php
<?php

class ExampleTest extends TestCase
{
    /**
     * 一个基本的功能测试示例。
     *
     * @return void
     */
    public function testBasicExample()
    {
        // 断言列表中的每个用户至少具有 id、name 和 email 属性。
        $this->get('/users')
             ->seeJsonStructure([
                 '*' => [
                     'id', 'name', 'email'
                 ]
             ]);
    }
}

您还可以嵌套 * 符号。在这种情况下,我们将断言 JSON 响应中的每个用户包含一组给定的属性,并且每个用户的每个宠物也包含一组给定的属性:

php
$this->get('/users')
     ->seeJsonStructure([
         '*' => [
             'id', 'name', 'email', 'pets' => [
                 '*' => [
                     'name', 'age'
                 ]
             ]
         ]
     ]);

会话 / 认证

Laravel 提供了几种用于在测试期间处理会话的辅助工具。首先,您可以使用 withSession 方法将会话数据设置为给定数组。这对于在测试请求应用程序之前加载会话数据非常有用:

php
<?php

class ExampleTest extends TestCase
{
    public function testApplication()
    {
        $this->withSession(['foo' => 'bar'])
             ->visit('/');
    }
}

当然,会话的一个常见用途是维护用户状态,例如已认证的用户。actingAs 辅助方法提供了一种简单的方法来将给定用户认证为当前用户。例如,我们可以使用模型工厂生成并认证用户:

php
<?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 方法来指定应使用哪个守卫来认证给定用户:

php
$this->actingAs($user, 'backend')

禁用中间件

在测试应用程序时,您可能会发现禁用某些测试的中间件很方便。这将允许您在不考虑中间件的情况下测试路由和控制器。Laravel 包含一个简单的 WithoutMiddleware trait,您可以使用它来自动禁用测试类的所有中间件:

php
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use WithoutMiddleware;

    //
}

如果您只想为少数测试方法禁用中间件,可以在测试方法中调用 withoutMiddleware 方法:

php
<?php

class ExampleTest extends TestCase
{
    /**
     * 一个基本的功能测试示例。
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->withoutMiddleware();

        $this->visit('/')
             ->see('Laravel 5');
    }
}

自定义 HTTP 请求

如果您想向应用程序发出自定义 HTTP 请求并获取完整的 Illuminate\Http\Response 对象,可以使用 call 方法:

php
public function testApplication()
{
    $response = $this->call('GET', '/');

    $this->assertEquals(200, $response->status());
}

如果您正在进行 POSTPUTPATCH 请求,可以在请求中传递一个输入数据数组。当然,这些数据将在您的路由和控制器中通过请求实例可用:

php
$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 的记录,我们可以这样做:

php
public function testDatabase()
{
    // 调用应用程序...

    $this->seeInDatabase('users', ['email' => 'sally@example.com']);
}

当然,seeInDatabase 方法和其他类似的辅助工具是为了方便。您可以自由使用 PHPUnit 的任何内置断言方法来补充您的测试。

在每次测试后重置数据库

在每次测试后重置数据库通常很有用,以便前一个测试的数据不会干扰后续测试。

使用迁移

一种选择是在每次测试后回滚数据库,并在下一个测试之前迁移它。Laravel 提供了一个简单的 DatabaseMigrations trait,可以自动为您处理此问题。只需在测试类上使用该 trait:

php
<?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
<?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');
    }
}
lightbulb

此 trait 仅将默认数据库连接包装在事务中。

模型工厂

在测试时,通常需要在执行测试之前向数据库插入一些记录。与其在创建此测试数据时手动指定每个列的值,Laravel 允许您使用“工厂”为每个 Eloquent 模型定义一组默认属性。要开始,请查看应用程序中的 database/factories/ModelFactory.php 文件。开箱即用,此文件包含一个工厂定义:

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.phpCommentFactory.php 文件。

多种工厂类型

有时,您可能希望为同一个 Eloquent 模型类拥有多个工厂。例如,您可能希望除了普通用户之外,还拥有“管理员”用户的工厂。您可以使用 defineAs 方法定义这些工厂:

php
$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 方法检索基本属性。一旦拥有了属性,只需补充任何所需的额外值:

php
$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
    $user = $factory->raw(App\User::class);

    return array_merge($user, ['admin' => true]);
});

在测试中使用工厂

一旦定义了工厂,您可以在测试或数据库种子文件中使用它们来生成模型实例,使用全局 factory 函数。因此,让我们看看创建模型的一些示例。首先,我们将使用 make 方法,该方法创建模型但不将其保存到数据库:

php
public function testDatabase()
{
    $user = factory(App\User::class)->make();

    // 在测试中使用模型...
}

如果您想覆盖模型的某些默认值,可以将值数组传递给 make 方法。只有指定的值会被替换,而其余的值将保持为工厂指定的默认值:

php
$user = factory(App\User::class)->make([
    'name' => 'Abigail',
   ]);

您还可以创建多个模型的集合或创建给定类型的模型:

php
// 创建三个 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 方法将它们保存到数据库:

php
public function testDatabase()
{
    $user = factory(App\User::class)->create();

    // 在测试中使用模型...
}

同样,您可以通过将数组传递给 create 方法来覆盖模型上的属性:

php
$user = factory(App\User::class)->create([
    'name' => 'Abigail',
   ]);

向模型添加关系

您甚至可以将多个模型持久化到数据库中。在此示例中,我们甚至会将关系附加到创建的模型上。使用 create 方法创建多个模型时,将返回一个 Eloquent 集合实例,允许您使用集合提供的任何方便函数,例如 each

php
$users = factory(App\User::class, 3)
           ->create()
           ->each(function ($u) {
                $u->posts()->save(factory(App\Post::class)->make());
            });

关系和属性闭包

您还可以使用工厂定义中的闭包属性将关系附加到模型上。例如,如果您想在创建 Post 时创建一个新的 User 实例,可以执行以下操作:

php
$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});

这些闭包还会接收包含它们的工厂的评估属性数组:

php
$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
<?php

class ExampleTest extends TestCase
{
    public function testUserRegistration()
    {
        $this->expectsEvents(App\Events\UserRegistered::class);

        // 测试用户注册...
    }
}

您可以使用 doesntExpectEvents 方法验证给定事件触发:

php
<?php

class ExampleTest extends TestCase
{
    public function testPodcastPurchase()
    {
        $this->expectsEvents(App\Events\PodcastWasPurchased::class);

        $this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);

        // 测试购买播客...
    }
}

如果您想阻止所有事件处理程序运行,可以使用 withoutEvents 方法:

php
<?php

class ExampleTest extends TestCase
{
    public function testUserRegistration()
    {
        $this->withoutEvents();

        // 测试用户注册代码...
    }
}

模拟任务

有时,您可能只想测试在向应用程序发出请求时控制器是否调度了特定任务。这允许您在隔离的情况下测试路由/控制器 - 与任务的逻辑分开。当然,您可以在单独的测试类中测试任务本身。

Laravel 提供了一个方便的 expectsJobs 方法,可以验证预期任务是否被调度,但任务本身不会被执行:

php
<?php

class ExampleTest extends TestCase
{
    public function testPurchasePodcast()
    {
        $this->expectsJobs(App\Jobs\PurchasePodcast::class);

        // 测试购买播客代码...
    }
}
lightbulb

此方法仅检测通过 DispatchesJobs trait 的调度方法或 dispatch 辅助函数调度的任务。它不检测直接发送到 Queue::push 的任务。

模拟 Facades

在测试时,您可能经常想要模拟对 Laravel facade 的调用。例如,考虑以下控制器操作:

php
<?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
<?php

class FooTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $this->visit('/users')->see('value');
    }
}
lightbulb

您不应模拟 Request facade。相反,在运行测试时,将所需的输入传递给 HTTP 辅助方法,如 callpost