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

底层实现

上一篇教程我们介绍了在 Laravel 框架中如何基于 PHPUnit 编写单元测试,其实单元测试基本上使用的都是 PHPUnit 框架提供的原生方法,今天我们来看下 Laravel 如何基于 PHPUnit 实现 HTTP 功能测试。

Laravel 框架开箱为我们提供了一个功能测试用例示例 tests/Feature/ExampleTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

功能测试类都位于 tests/Feature 目录下,和单元测试类一样,也继承自 Tests\TestCase,从根源上都继承自 PHPUnit\Framework\TestCase,只不过功能测试用到的很多方法都是 Laravel 自行封装实现的,这些实现都是通过独立的 Trait 来完成,在 Illuminate\Foundation\Testing\TestCase 中,可以看到这些 Trait 的引入:

use Concerns\InteractsWithContainer,
    Concerns\MakesHttpRequests,
    Concerns\InteractsWithAuthentication,
    Concerns\InteractsWithConsole,
    Concerns\InteractsWithDatabase,
    Concerns\InteractsWithExceptionHandling,
    Concerns\InteractsWithSession,
    Concerns\MocksApplicationServices;

比如请求相关的测试方法都位于 Illuminate\Foundation\Testing\Concerns\MakesHttpRequests 中,认证相关的测试方法都位于 Illuminate\Foundation\Testing\Concerns\InteractsWithAuthentication 中,会话相关的测试方法都位于 Illuminate\Foundation\Testing\Concerns\InteractsWithSession 中,而响应相关的测试方法都位于 Illuminate\Foundation\Testing\TestResponse 中,该实例会在调用 HTTP 功能测试类中调用 $this->get 方法时返回(当然,还支持 postputdeletegetJson 等类似方法,这些方法都定义在 Illuminate\Foundation\Testing\Concerns\MakesHttpRequests 中)。

接下来,我们就基于 Laravel 提供的这些测试方法对 HTTP 请求和响应进行测试。

基本测试

如 Laravel 提供的示例代码所示,我们可以从最简单的测试开始,测试响应的状态码,与之等价的,我们还可以通过 assertOk 方法断言响应状态码是否是 200:

public function testBasic()
{
    $response = $this->get('/');

    $response->assertOk();  // 返回状态码是否是 200
}

与状态码相关的还有一系列方法,这里简单做个列举:

  • assertSuccessful:断言响应状态码是否介于200-300之间;
  • assertNotFound:断言响应状态码是否是 404;
  • assertForbidden:断言响应状态码是否是 403;

此外,我们还可以通过 assertSeeassertSeeText 方法断言响应实体中是否包含给定字符串:

public function testSeeText()
{
    $response = $this->get('/');

    $response->assertSee('Laravel');

    $response->assertSeeText('Laravel');
}

注:上述两个方法的区别是后者会将响应实体转化为纯文本进行判断,即将 HTML 标签过滤掉。

与之相对的,还有 assertDontSeeassertDontSeeText 方法,与上述判断相反,断言响应实体中不包含给定字符串。与之类似的,还有 assertSeeInOrder 以及 assertSeeTextInOrder 方法,用于断言给定字符串是否按照对应的顺序出现在响应实体中。

测试重定向

我们可以通过 assertRedirect 对重定向响应进行测试,断言重定向指向的 URL 是否与预期一致:

public function testRedirection()
{
    $response = $this->get('/redirect');

    $response->assertRedirect('https://xueyuanjun.com');
}

要测试这个重定向响应,我们需要确保在 routes/web.php 中包含如下路由定义:

Route::get('/redirect', function () {
    return redirect('https://xueyuanjun.com');
});

然后,我们运行 phpunit 就可以让测试通过了:

此外,还有一个与之类似的方法 assertLocation 也可以用于断言重定向 URL,与 assertRedirect 不同之处在于,它不会对响应状态码和响应头进行判断,assertRedirect 会先断言响应状态码是否在 [201, 301, 302, 303, 307, 308] 数组中并且响应头中包含 Location 字段。

测试响应头

如果你想要对响应头进行深入测试,可以通过 assertHeader 方法实现:

public function testHeader()
{
    $response = $this->get('/header');

    $response->assertHeader('X-Header-One', 'Laravel学院')
        ->assertHeader('X-Header-Two', 'HTTP 功能测试');
}

为了让上述测试用例通过,我们还要在 routes/web.php 中定义如下路由:

Route::get('/header', function (){
    return response('测试响应头')
        ->header('X-Header-One', 'Laravel学院')
        ->header('X-Header-Two', 'HTTP 功能测试');
});

运行 phpunit 命令,结果如下,表示测试通过:

public function testCookie()
{
    $response = $this->get('/cookie');

    $response->assertCookie('UserName', '学院君');
}

相应的,我们在 routes/web.php 中新增如下路由:

Route::get('/cookie', function (){
    return response('测试 Cookie')->cookie('UserName', '学院君');
});

运行 phpunit,测试通过:

  • assertCookieExpired:断言给定 Cookie 是否过期;
  • assertCookieNotExpired:断言给定 Cookie 没有过期;
  • assertCookieMissing:断言给定 Cookie 不存在;
  • assertPlainCookie:断言给定 Cookie 存在且与给定值匹配(不加密)。

测试 Session

为了测试 HTTP Session,我们先在 routes/web.php 中定义一个新的路由:

Route::get('/session', function (){
    session(['SiteName' => 'Laravel学院']);
    session(['UserName' => '学院君']);
    return response('测试 Session');
});

然后为这个路由编写测试用例:

public function testSession()
{
    $response = $this->get('/session');

    $response->assertSessionHas('SiteName', 'Laravel学院')
        ->assertSessionHas('UserName')
        ->assertSessionMissing('AppName');

    // 一次性指定包含的 Session
    $response->assertSessionHasAll(['SiteName' => 'Laravel学院', 'UserName' => '学院君']);
}

我们可以通过 assertSessionHas 方法依次断言每个 Session 存储项,也可以通过 assertSessionHasAll 方法一次性断言多个 Session 存储项,在使用它们的时候,可以指定对应的 Session 值,也可以不指定,如果指定的话则必须与存储的 Session 之匹配才会测试通过。

另外,我们可以通过 assertSessionMissing 方法断言指定 Session 存储项不存在。

表单验证错误信息

此外,我们知道,表单验证的错误信息也是存储在一次性 Session 里面的,Laravel 单独为我们提供了相关的断言方法对表单验证场景进行测试,下面我们通过一个简单的例子来演示。

首先我们在 routes/web.php 中定义一个处理表单提交的路由:

Route::post('/form', function (\Illuminate\Http\Request $request) {
    $request->validate([
        'title' => 'required|max:200',
        'body' => 'required'
    ]);
    return response('测试表单验证');
});

在这个路由闭包中,我们会对表单请求字段进行验证,对于不符合指定规则的字段会返回验证错误信息。然后我们在测试用例中编写相应的测试方法:

public function testForm()
{
    $response = $this->post('/form', ['title' => '学院君', 'content' => '测试数据']);

    $response->assertSessionHasErrors(['body' => 'The body field is required.'])
        ->assertSessionDoesntHaveErrors(['content' => 'The content field is required.']);
}

Laravel 会将验证错误信息存放到名为 errors 的 Session 项中,所以我们通过 assertSessionHasErrorsassertSessionDoesntHaveErrors 断言 Session 中是否包含/不包含对应的验证错误信息。这两个方法都支持一次性断言多个字段。

除此之外,与验证错误 Session 相关的测试方法还有用于断言验证结果不包含错误信息的 assertSessionHasNoErrors,以及用于断言错误信息位于指定数组的 assertSessionHasErrorsIn,该方法可以通过 assertSessionHasErrors 来替代。

下一篇我们将继续围绕 HTTP 功能测试展开,介绍如何在 Laravel 中测试用户认证、视图、文件上传和 JSON API。

上一篇: 在 Laravel 中基于 PHPUnit 进行代码测试:单元测试篇

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