8.测试时如何处理异常
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 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
在之前的章节中,我们为新建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
是不会遇到什么功能上的问题的:
在当前情况下,我们在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');
}
.
.
让我们来运行一下测试:
抛出了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');
}
.
.
再次测试,测试通过: