Laravel 專案:使用 TDD 構建論壇 Chapter 8

洛未必達發表於2018-04-30

0.寫在前面

  • 本系列文章為laracasts.com 的系列視訊教程——Let's Build A Forum with Laravel and TDD 的學習筆記。若喜歡該系列視訊,可去該網站訂閱後下載該系列視訊, 支援正版
  • 視訊原始碼地址:https://github.com/laracasts/Lets-Build-a-Forum-in-Laravel
  • *本專案為一個 forum(論壇)專案,與本站的第二本實戰教程 Laravel 教程 - Web 開發實戰進階 ( Laravel 5.5 ) 類似,可互相參照
  • 專案開發模式為TDD開發,教程簡介為:

    A forum is a deceptively complex thing. Sure, it's made up of threads and replies, but what else might exist as part of a forum? What about profiles, or thread subscriptions, or filtering, or real-time notifications? As it turns out, a forum is the perfect project to stretch your programming muscles. In this series, we'll work together to build one with tests from A to Z.

  • 專案版本為laravel 5.4,教程後面會進行升級到laravel 5.5的教學
  • 視訊教程共計 102 個小節,章節內容與視訊教程一一對應

1.本節說明

  • 對應視訊第 8 小節:The Exception Handling Conundrum

2.本節內容

目前我們有關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/{thread}', 'threadsController@show')->name('threads.show');
Route::get('/threads/create', 'threadsController@create')->name('threads.create');
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']);
    }
.
.

我們來編寫一個功能測試,用來測試未登入使用者訪問 http://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);
}
.
.

在類似於未登入使用者訪問 http://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->oldExceptionHander = $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()方法。該方法對Handler.php的內容進行了重寫,預設丟擲異常。當我們不需要丟擲異常時,繼續呼叫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

3.寫在後面

  • 如有建議或意見,歡迎指出~
  • 如果覺得文章寫的不錯,請點贊鼓勵下哈,你的鼓勵將是我的動力!
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章