在 Laravel 中基于 PHPUnit 进行代码测试:HTTP 测试篇(下)


我们继续在上一篇教程的基础上演示如何编写 HTTP 功能测试用例。

测试用户认证

Session 除了用于存储一次性验证错误信息和用户会话状态外,最主要的用途就是维护用户认证的状态,在 Laravel 中,可以通过框架提供的一系列断言方法对用户认证状态进行测试。

我们基于 Laravel 框架提供的认证脚手架中的路由进行测试,如果你还没有在测试项目中生成认证脚手架代码,运行如下 Artisan 命令生成:

php artisan make:auth

这样,在 routes/web.php 中就会多出如下路由定义代码:

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Auth::routes() 中包含了用户注册、登录相关的路由。

然后我们来编写用户注册登录相关的测试用例,对用户注册、登录功能进行测试,由于涉及到多个测试用例,我们编写一个新的功能测试类:

php artisan make:test AuthTest

该命令会在 tests/Feature 目录下创建一个 AuthTest 类,我们在这个测试类中编写一系列测试用例如下:

<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;

class AuthTest extends TestCase
{
    /**
     * 测试注册表单页
     */
    public function testRegisterForm()
    {
        $response = $this->get('/register');

        $response->assertOk();
    }

    /**
     * 测试用户提交注册表单
     */
    public function testPostRegister()
    {
        $response = $this->post('/register', [
            'name' => 'test',
            'email' => 'test@laravel58.test',
            'password' => 'password',  // Laravel 5.8 验证表单要求密码长度不低于 8 位
            'password_confirmation' => 'password'
        ]);

        $response->assertRedirect('/');
    }

    /**
     * 测试登录表单页
     */
    public function testLoginForm()
    {
        $response = $this->get('/login');

        $response->assertSuccessful();
    }

    /**
     * 测试用户提交登录表单
     */
    public function testPostLogin()
    {
        $user = User::where('name', 'test')->first();

        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        $response->assertRedirect('/home');  // 用户登录成功后跳转到 /home
    }

    /**
     * 测试未认证状态下访问 /home 路由
     */
    public function testHomeWithoutAuthenticated()
    {
        $response = $this->get('/home');

        $response->assertRedirect('/login');  // 用户未认证则跳转到登录页
        $this->assertGuest();  // 断言用户未认证
        // 断言给定认证凭证是否匹配
        $this->assertCredentials([
            'email' => 'test@laravel58.test',
            'password' => 'password'
        ]);
    }

    /**
     * 测试认证状态下访问 /home 路由
     */
    public function testHomeWithAuthenticated()
    {
        $user = User::where('name', 'test')->first();
        $this->actingAs($user)->get('/home');
        $this->assertAuthenticated('web')
            ->assertAuthenticatedAs($user);  // 断言用户认证且以 $user 身份认证
    }
}

在认证用户时,我们可以通过模拟提交表单请求或者直接通过 Laravel 提供的 actionAs 方法,使用 actionAs 方法进行模拟认证时,可以通过以下方法断言用户认证:

  • assertAuthenticated:断言用户是否认证,默认参数为 guard=web,你可以指定其他的 guard,与之相对的断言方法是 assertGuest,同样,你可以传参指定认证 guard
  • assertAuthenticatedAs:断言用户是否以指定用户实例认证,你可以通过第二个参数指定相应的 guard 参数;

此外,还可以通过 assertCredentials 方法断言给定认证凭证是否与数据库记录匹配。

我们在命令行中运行 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');  // 伪造上传图片

    $this->post('/photo', [
        'photo' => $photo
    ]);

    Storage::disk('photos')->assertMissing('picture.jpg');   // 断言文件是否上传成功
}

为了让测试用例通过,需要在 routes/web.php 中定义一个路由:

Route::post('/photo', function (\Illuminate\Http\Request $request){
    $request->validate([
        'photo' => 'required|image|mimes:jpeg,bmp,png,gif'
    ]);
    $photo = $request->file('photo');
    $basename = $photo->getClientOriginalName();

    return $photo->storeAs('photos', $basename);
});

需要注意的是,通过 Storage::fake 方法伪造的 photos 目录位于 storage/framework/testing/disks/photos 而不是 storage/app/photos,所以上述测试用例会通过。与之相对的,如果要断言指定文件存在,可以使用 Storage::disk('photos')->assertExists 方法。

除了图片之外,你还可以通过 UploadedFile::fake()->create() 方法伪造其他类型的文件。

测试视图模板

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 方法的第一个参数值。

测试 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 请求方法,比如 GETPOSTPUTDELETE 等,也可以通过形如 getJsonpostJsonputJsondeleteJson 这些方法,这样,就不需要传递请求方法参数了。

以上面这个 API 路由为例,我们可以这样为其编写测试用例:

<?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->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 方法相对。

好了,关于 HTTP 功能测试学院君就简单介绍到这里,下一篇开始我们将会基于 Laravel Dusk 对模拟浏览器进行测试,该功能与 HTTP 测试的区别是可以模拟客户端行为,比如点击、下拉、刷新、键盘输入等。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 在 Laravel 中基于 PHPUnit 进行代码测试:HTTP 测试篇(上)

>> 下一篇: 在 Laravel 中基于 Dusk 实现浏览器自动化测试快速入门