8.测试时如何处理异常

未匹配的标注

本节说明

  • 对应视频第 8 小节:The Exception Handling Conundrum

本节内容

目前我们有关Thread的路由为:
web.php

.
.
Route::get('/threads','ThreadsController@index');
Route::post('/threads','ThreadsController@store');
Route::get('/threads/{thread}','ThreadsController@show');
.
.

对于Thread,我们有完整的CURD操作,可以将Thread视作一个资源。

Laravel 遵从 RESTful 架构的设计原则,将数据看做一个资源,由 URI 来指定资源。对资源进行的获取、创建、修改和删除操作,分别对应 HTTP 协议提供的 GET、POST、PATCH 和 DELETE 方法。当我们要查看一个 id 为 1 的用户时,需要向 /users/1 地址发送一个 GET 请求,当 Laravel 的路由接收到该请求时,默认会把该请求传给控制器的 show 方法进行处理。

因此我们将与Thread相关的路由简写为:

Route::resource('threads','ThreadsController');

以上代码等同于:

Route::get('/threads', 'threadsController@index')->name('threads.index');
Route::get('/threads/create', 'threadsController@create')->name('threads.create');
Route::get('/threads/{thread}', 'threadsController@show')->name('threads.show');
Route::post('/threads', 'threadsController@store')->name('threads.store');
Route::get('/threads/{thread}/edit', 'threadsController@edit')->name('threads.edit');
Route::patch('/threads/{thread}', 'threadsController@update')->name('threads.update');
Route::delete('/threads/{thread}', 'threadsController@destroy')->name('threads.destroy');

运行测试:

$ APP_ENV=testing phpunit

file
在之前的章节中,我们为新建thread编写了测试代码,且成功通过。现在让我们完成此功能的代码:
\app\Http\Controllers\ThreadsController.php

.
.
public function create()
{
    return view('threads.create');
}
.
.

\resources\views\threads\create.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Create a New Thread</div>

                    <div class="panel-body">
                        <form method="post" action="/threads">
                            {{ csrf_field() }}

                            <div class="form-group">
                                <label for="title">Title</label>
                                <input type="text" class="form-control" id="title" name="title">
                            </div>

                            <div class="form-group">
                                <label for="body">Body</label>
                                <textarea name="body" id="body" class="form-control" rows="8"></textarea>
                            </div>

                            <button type="submit" class="btn btn-primary">Publish</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

我们在编写代码之前就为thread的新建动作编写了测试,且成功通过,这意味着我们发布thread是不会遇到什么功能上的问题的:
file
file
file
在当前情况下,我们在ThreadsController__construct方法中使用了白名单机制only来做权限控制,但这样不太安全。我们现在改用黑名单机制,即除了index,show方法,其他方法都需要进行登录才能操作。
\app\Http\Controllers\ThreadsController.php

class ThreadsController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except(['index','show']);
    }
.
.

我们来编写一个功能测试,用来测试未登录用户访问 forum.test/threads/create 页面。测试逻辑应为:用户访问页面,如未登录,重定向到 登录页面
\tests\Feature\CreateThreadsTest.php

.
.
/** @test */
public function guests_may_not_see_the_create_thread_page()
{
    $this->get('/threads/create')
        ->assertRedirect('/login');
}
.
.

让我们来运行一下测试:

file
抛出了Unauthenticated的异常,这与我们的测试初衷不符。我们曾在Handler.php做过设置,如果app()->environment() === 'testing'就会抛出异常:
\app\Exceptions\Handler.php

.
.
public function render($request, Exception $exception)
{
    if (app()->environment() === 'testing') throw $exception;

    return parent::render($request, $exception);
}
.
.

在类似于未登录用户访问 forum.test/threads/create 页面的测试中,我们需要放行,而不是抛出异常。现在我们来进行处理:
\forum\tests\TestCase.php

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Contracts\Debug\ExceptionHandler;
use App\Exceptions\Handler;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected function setUp()
    {
        parent::setUp();

        $this->disableExceptionHandling();
    }

    protected function signIn($user = null)
    {
        $user = $user ?: create('App\User');

        $this->actingAs($user);

        return $this;
    }

    protected function disableExceptionHandling()
    {
        $this->oldExceptionHandler = $this->app->make(ExceptionHandler::class);

        $this->app->instance(ExceptionHandler::class,new class extends Handler{
           public function __construct(){}
           public function report(\Exception $e){}
           public function render($request,\Exception $e){
               throw $e;
           }
        });
    }

    protected function withExceptionHandling()
    {
        $this->app->instance(ExceptionHandler::class,$this->oldExceptionHandler);

        return $this;
    }
}

注:不要忘了头部的引用

对于继承TestCase基类的测试用例,我们默认先调用disableExceptionHandling()方法,在该方法中,我们首先将默认的异常处理器记录到oldExceptionHandler属性中,然后绑定了一个我们自定义的异常处理器到应用中。在这个自定义的处理器中,我们对Handler.php的内容进行了重写,注意render()方法,在我们重写的该方法中,只会抛出异常,而不是转化为 HTTP 响应。当我们不需要抛出异常,而需要得到 HTTP 响应时,继续调用withExceptionHandling()方法,在该方法中,我们重新将默认的异常处理器绑定到应用当中。
现在将\app\Exceptions\Handler.php中的下面这行代码删掉:

if (app()->environment() === 'testing') throw $exception;

同时在编写的测试用例中调用withExceptionHandling()方法:
\tests\Feature\CreateThreadsTest.php

.
.
/** @test */
public function guests_may_not_see_the_create_thread_page()
{
    $this->withExceptionHandling() // 此处调用
        ->get('/threads/create')
        ->assertRedirect('/login');
}
.
.

再次测试,测试通过:
file

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:2