[ Lumen 5.2 文档 ] 更多特性 —— 单元测试

1、简介

Lumen植根于测试,实际上,使用PHPUnit对测试提供支持是开箱即用的,并且测试配置文件phpunit.xml已经为应用设置好了。框架还提供了很多辅助函数从而允许你对应用进行更加富有表现力的测试。

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

1.1 测试环境

Lumen在测试时自动配置缓存驱动为数组驱动,这意味着测试时不会持久化存储缓存数据。

如果需要的话你可以自由创建其它测试环境配置。testing环境变量可以在phpunit.xml文件中配置。

1.2 定义&运行测试

要创建一个测试用例,只需简单在tests目录创建一个新的测试文件,测试类应该继承TestCase,然后你可以使用PHPUnit定义测试方法。要运行测试,简单从终端执行phpunit命令即可:

<?php

class FooTest extends TestCase{
    public function testSomethingIsTrue()
    {
        $this->assertTrue(true);
    }
}

注意:如果你在测试类中定义自己的setUp方法,确保在其中调用parent::setUp

2、应用测试

Lumen为生成HTTP请求、测试输出提供了流接口API(方法链)。

2.1 测试 JSON API

Lumen还提供多个辅助函数用于测试JSON API及其响应。例如,getpostputpatch, 和 delete方法用于通过多种HTTP请求方式发出请求。你还可以轻松传递数据和头到这些方法。作为开始,我们编写测试来生成POST请求到/user并断言返回的数据是否是JSON格式:

<?php

class ExampleTest extends TestCase{
    /**
     * 基本功能测试示例
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->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->post('/user', ['name' => 'Sally'])
             ->seeJsonEquals([
                 'created' => true,
             ]);
    }
}

2.2 认证

辅助函数actingAs 提供了认证给定用户为当前用户的简单实现:

<?php

class ExampleTest extends TestCase{
    public function testApplication()
    {
        $user = factory('App\User')->create();

        $this->actingAs($user)
             ->get('/user')
    }
}

2.3 自定义HTTP请求

如果你想要在应用中生成自定义HTTP请求并获取完整的Illuminate\Http\Response对象,可以使用call方法:

public function testApplication(){
    $response = $this->call('GET', '/');
    $this->assertEquals(200, $response->status());
}

如果你要生成POSTPUT, 或者 PATCH请求可以在请求中传入输入数据数组,在路由或控制器中可以通过Request实例访问请求数据:

$response = $this->call('POST', '/user', ['name' => 'Taylor']);

3、处理数据库

Lumen还提供了多种有用的工具让测试数据库驱动的应用更加简单。首先,你可以使用帮助函数seeInDatabase来断言数据库中的数据是否和给定数据集合匹配。例如,如果你想要通过email值为sally@example.com的条件去数据表users查询是否存在该记录 ,我们可以这样做:

public function testDatabase(){
    // 调用应用...
    $this->seeInDatabase('users', ['email' => 'sally@foo.com']);
}

当然,seeInDatabase方法和其他类似帮助函数都是为了方便,你还可以使用所有PHPUnit内置的断言函数来补充测试。

3.1 每次测试后重置数据库

每次测试后重置数据库通常很有用,这样的话上次测试的数据不会影响下一次测试。

使用迁移
一种方式是每次测试后回滚数据库并在下次测试前重新迁移。Lumen提供了一个简单的DatabaseMigrations trait来自动为你处理。在测试类上简单使用该trait如下:
<?php

use Laravel\Lumen\Testing\DatabaseMigrations;use Laravel\Lumen\Testing\DatabaseTransactions;

class ExampleTest extends TestCase{
    use DatabaseMigrations;

    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->get('/foo');
    }}
使用事务
另一种方式是将每一个测试用例包裹到一个数据库事务中,Lumen提供了方便的 DatabaseTransactions trait自动为你处理:
<?php

use Laravel\Lumen\Testing\DatabaseMigrations;use Laravel\Lumen\Testing\DatabaseTransactions;

class ExampleTest extends TestCase{
    use DatabaseTransactions;

    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->get('/foo');
    }}

3.2 模型工厂

测试时,通常需要在执行测试前插入新数据到数据库。在创建测试数据时,Lumen允许你使用”factories”为每个Eloquent模型定义默认的属性值集合,而不用手动为每一列指定值。作为开始,我们看一下atabase/factories/ModelFactory.php文件,该文件包含了一个工厂定义:

$factory->define(App\User::class, function ($faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
    ];
});

在闭包中,作为工厂定义,我们返回该模型上所有属性默认测试值。该闭包接收PHP库Faker实例,从而允许你方便地为测试生成多种类型的随机数据。

当然,你可以添加更多工厂到ModelFactory.php文件。

多个工厂类型

有时候你可能想要为同一个Eloquent模型类生成多个工厂,例如,除了正常用户外可能你想要为“管理员”用户生成一个工厂,你可以使用defineAs方法定义这些工厂:

$factory->defineAs(App\User::class, 'admin', function ($faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        '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',
]);

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

// 创建3个 App\User 实例...
$users = factory(App\User::class, 3)->make();
// 创建1个 App\User "admin" 实例...
$user = factory(App\User::class, 'admin')->make();
// 创建3个 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());
            });

4、模拟

4.1 模拟事件

如果你在重度使用Lumen的事件系统,可能想要在测试时模拟特定事件。例如,如果你在测试用户注册,你可能不想所有UserRegistered的时间处理器都被触发,因为这可能会发送欢迎邮件,等等。

Lumen提供可一个方便的expectsEvents方法来验证期望的事件被触发,但同时阻止该事件的其它处理器运行:

<?php

class ExampleTest extends TestCase{
    public function testUserRegistration()
    {
        $this->expectsEvents(App\Events\UserRegistered::class);
        // 测试用户注册代码...
    }
}

如果你想要阻止所有事件运行,可以使用withoutEvents方法:

<?php

class ExampleTest extends TestCase{
    public function testUserRegistration()
    {
        $this->withoutEvents();
        // 测试用户注册代码...
    }
}

4.2 模拟队列任务

有时候,你可能想要在请求时简单测试控制器分发的指定任务,这允许你孤立的测试路由/控制器——将其从任务逻辑中分离出去,当然,接下来你可以在一个独立测试类中测试任务本身。

Lumen提供了一个方便的expectsJobs方法来验证期望的任务被分发,但该任务本身不会被测试:

<?php

class ExampleTest extends TestCase{
    public function testPurchasePodcast()
    {
        $this->expectsJobs(App\Jobs\PurchasePodcast::class);
        // 测试购买播客代码...
    }
}

注意:这个方法只检查通过全局辅助函数dispatch或路由/控制器中调用$this->dispatch分发的任务,并不检查直接通过Queue::push分发的任务。

4.3 模拟门面

测试的时候,你可能经常想要模拟Lumen门面的调用,例如,看看下面的控制器动作:

<?php

namespace App\Http\Controllers;

use Cache;
use Illuminate\Routing\Controller;

class UserController extends Controller{
    /**
     * 显示应用用户列表
     *
     * @return Response
     */
    public function index()
    {
        $value = Cache::get('key');

        //
    }
}

我们可以通过使用shouldReceive方法模拟Cache门面的调用,该方法返回一个Mockery模拟的实例,由于门面通过Lumen服务容器解析和管理,它们比通常的静态类更具有可测试性。例如,我们来模拟Cache门面的调用:

<?php

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

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

注意:不要模拟Request门面,取而代之地,在测试时传递输入到HTTP辅助函数如callpost

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长