Laravel TDD 練習
TDD
目的:
跟著 Laravel 5 中的 TDD 觀念與實戰 跑一次 Laravel 中的簡易 TDD,順便蒐集相關 TDD 練習資料
references:
[隨筆] The Three Laws of TDD-從紅燈變綠燈的過程
Laravel 5 中的 TDD 觀念與實戰
http://docs.mockery.io/en/latest/index.html
https://laravel.com/docs/7.x/mocking#introduction
https://stackoverflow.com/questions/37734620/laravel-phpunit-always-passes-csrf
Tennis-Refactoring-Kata(各種語言重構範例)
建立專案
Laravel installer
composer global require laravel/installer
Via Composer Create-Project
composer create-project --prefer-dist laravel/laravel blog
cepar@DESKTOP-H3LUMQB MINGW64 ~/Desktop/projects/play-tdd
$ composer create-project --prefer-dist laravel/laravel blog
Creating a "laravel/laravel" project at "./blog"
Installing laravel/laravel (v7.12.0)
- Installing laravel/laravel (v7.12.0): Loading from cache
Created project in C:\Users\cepar\Desktop\projects\play-tdd\blog
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 97 installs, 0 updates, 0 removals
- Installing voku/portable-ascii (1.5.2): Loading from cache
- Installing symfony/polyfill-ctype (v1.17.1): Loading from cache
- Installing phpoption/phpoption (1.7.4): Loading from cache
- Installing vlucas/phpdotenv (v4.1.7): Loading from cache
- Installing symfony/css-selector (v5.1.2): Loading from cache
...
–prefer-source ? --prefer-dist ?
–prefer-source: There are two ways of downloading a package: source and dist. For stable versions Composer will use the dist by default. The source is a version control repository. If --prefer-source is enabled, Composer will install from source if there is one. This is useful if you want to make a bugfix to a project and get a local git clone of the dependency directly.
–prefer-dist: Reverse of --prefer-source, Composer will install from dist if possible. This can speed up installs substantially on build servers and other use cases where you typically do not run updates of the vendors. It is also a way to circumvent problems with git if you do not have a proper setup.
–prefer-dist 直接抓該套件的 distribution 版本且緩存,下次安裝就會直接從本機緩存安裝,安裝速度快,但抓下來的版本沒有保留 .git,所以通常使用 dist 後不會再去更新 vendors,另外也可以避免環境中沒有 git 的問題。
https://getcomposer.org/doc/03-cli.md#install
測試專案正常啟動
$ php artisan serve
Laravel development server started: http://127.0.0.1:8000
[Fri Jul 3 13:03:35 2020] 127.0.0.1:52275 [200]: /favicon.ico
Model
在開始測試前 Laravel 已經有一些預設的測試案例了,先跑跑看…
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 803 ms, Memory: 20.00 MB
OK (2 tests, 2 assertions)
phpunit.xml 中定義了一些參數,其中 database 為了跟正是環境分開,使用了 sqlite 這組 connection 設定,
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
詳細 database connection 設定內容可以參照 config/database.php
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
建立一個 model,其中 -m 參數為順便建立 migration 檔案
$ php artisan make:model Article -m
Model created successfully.
Created Migration: 2020_07_03_052244_create_articles_table
執行完會發現多了這兩個檔案
app/Article.php
database/migrations/2020_07_03_052244_create_articles_table.php
修改 Model,設定可以被修改的欄位,保護其他欄位不會被修改
class Article extends Model
{
protected $fillable = ['title', 'body'];
}
更新 migration,替欲新增的 Article 資料庫結構加上 title 與 body 欄位
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
為了目錄結構乾淨,建立一個 Models 資料夾,並統一將 Model 放到下面管理
app/Models/Articel.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = ['title', 'body'];
}
為了讓每一次的測試都獨立互不影響,我們需要將每一次測試前的資料都重新建立,並在測試後還原
<?php
namespace Tests;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function initDatabase()
{
Artisan::call('migrate');
Artisan::call('db:seed');
}
protected function resetDatabase()
{
Artisan::call('migrate:reset');
}
}
在 Unit 底下 建立 ArticleTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Article;
class ArticleTest extends TestCase
{
protected function setUp():void
{
// 一定要先呼叫,建立 Laravel Service Container 以便測試
parent::setUp();
$this->initDatabase();
}
protected function tearDown():void
{
$this->resetDatabase();
}
public function testEmptyResult()
{
$articles = Article::all();
$this->assertEquals(0, count($articles));
}
public function testCreateAndList()
{
for ($i = 1; $i <= 10; $i ++) {
Article::create([
'title' => 'title ' . $i,
'body' => 'body ' . $i,
]);
}
$articles = Article::all();
$this->assertEquals(10, count($articles));
}
}
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 1.17 seconds, Memory: 26.00 MB
OK (4 tests, 4 assertions)
測試 repository
Repository 通常用來對 Model 進行操作,
在開始測試前,先建立 Repositories 目錄,並新增
ArticleRepository.php
建立 ArticleRepositoryTest.php 後,接著設定測試前後的環境與資料
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Article;
use App\Repositories\ArticleRepository;
class ArticleRepositoryTest extends TestCase
{
/**
* @var ArticleRepository
*/
protected $repository = null;
/**
* 建立 100 筆假文章
*/
protected function seedData()
{
for ($i = 1; $i <= 100; $i ++) {
Article::create([
'title' => 'title ' . $i,
'body' => 'body ' . $i,
]);
}
}
// 跟前面一樣,每次都要初始化資料庫並重新建立待測試物件
// 以免被其他 test case 影響測試結果
public function setUp():void
{
parent::setUp();
$this->initDatabase();
$this->seedData();
// 建立要測試用的 repository
$this->repository = new ArticleRepository();
}
public function tearDown():void
{
$this->resetDatabase();
$this->repository = null;
}
}
現在我們要在 Repository 中新增一個方法,可以取得最新的十筆文章。
首先 TDD 的第一步,在開始寫 code 之前先寫測試
public function testFetchLatest10Articles()
{
// 從 repository 中取得最新 10 筆文章
$articles = $this->repository->latest10();
$this->assertEquals(10, count($articles));
// 確認標題是從 100 .. 91 倒數
// "title 100" .. "title 91"
$i = 100;
foreach ($articles as $article) {
$this->assertEquals('title ' . $i, $article->title);
$i -= 1;
}
}
第二步,執行測試得到紅燈(測試不通過),這時候應該只能有一個紅燈,接著想辦法讓它變綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
E.... 5 / 5 (100%)
Time: 1.47 seconds, Memory: 28.00 MB
There was 1 error:
1) Tests\Unit\ArticleRepositoryTest::testFetchLatest10Articles
Error: Call to undefined method App\Repositories\ArticleRepository::latest10()
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:52
ERRORS!
Tests: 5, Assertions: 4, Errors: 1.
第三步,只針對紅燈原因修正,並且不應該去修正其他不相關的問題
<?php
namespace App\Repositories;
use App\Models\Article;
class ArticleRepository
{
public function latest10()
{
return Article::query()->orderBy('id', 'desc')->limit(9)->get();
}
}
回到第一步,並重複 1~3 反覆驗證結果直到綠燈(通過測試),流程如下:
紅燈
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
F.... 5 / 5 (100%)
Time: 1.45 seconds, Memory: 28.00 MB
There was 1 failure:
1) Tests\Unit\ArticleRepositoryTest::testFetchLatest10Articles
Failed asserting that 9 matches expected 10.
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:52
FAILURES!
Tests: 5, Assertions: 5, Failures: 1.
修正
<?php
namespace App\Repositories;
use App\Models\Article;
class ArticleRepository
{
public function latest10()
{
return Article::query()->orderBy('id', 'desc')->limit(10)->get();
}
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
..... 5 / 5 (100%)
Time: 1.45 seconds, Memory: 28.00 MB
OK (5 tests, 15 assertions)
ArticleRepositroy 新增 Create Method
先寫測試
ArticleRepositroyTest.php
public function testCreateArticles()
{
$maxId = Article::max('id');
$latestId = ++$maxId;
$article = $this->repository->create([
'title' => 'title ' . $latestId,
'body' => 'body ' . $latestId,
]);
$this->assertEquals($latestId, $article->id);
}
紅燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.E.... 6 / 6 (100%)
Time: 1.76 seconds, Memory: 28.00 MB
There was 1 error:
1) Tests\Unit\ArticleRepositoryTest::testCreateArticles
Error: Call to undefined method App\Repositories\ArticleRepository::create()
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:68
重構
ArticleRepositroy.php
public function create($attributes)
{
return Article::create($attributes);
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
...... 6 / 6 (100%)
Time: 1.73 seconds, Memory: 28.00 MB
OK (6 tests, 16 assertions)
Repository
Repository 用來對 Model 操作,
開始測試前,先建立 Repositories 目錄,並新增
ArticleRepository.php
建立 ArticleRepositoryTest.php ,設定測試前後準備
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Article;
use App\Repositories\ArticleRepository;
class ArticleRepositoryTest extends TestCase
{
/**
* @var ArticleRepository
*/
protected $repository = null;
/**
* 建立 100 筆假文章
*/
protected function seedData()
{
for ($i = 1; $i <= 100; $i ++) {
Article::create([
'title' => 'title ' . $i,
'body' => 'body ' . $i,
]);
}
}
// 跟前面一樣,每次都要初始化資料庫並重新建立待測試物件
// 以免被其他 test case 影響測試結果
public function setUp():void
{
parent::setUp();
$this->initDatabase();
$this->seedData();
// 建立要測試用的 repository
$this->repository = new ArticleRepository();
}
public function tearDown():void
{
$this->resetDatabase();
$this->repository = null;
}
}
現在我們要在 Repository 中新增一個方法,可以取得最新的十筆文章。
首先 TDD 的第一步,在開始寫 code 之前先寫測試
public function testFetchLatest10Articles()
{
// 從 repository 中取得最新 10 筆文章
$articles = $this->repository->latest10();
$this->assertEquals(10, count($articles));
// 確認標題是從 100 .. 91 倒數
// "title 100" .. "title 91"
$i = 100;
foreach ($articles as $article) {
$this->assertEquals('title ' . $i, $article->title);
$i -= 1;
}
}
第二步,執行測試得到紅燈(測試不通過),這時候應該只能有一個紅燈,接著想辦法讓它變綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
E.... 5 / 5 (100%)
Time: 1.47 seconds, Memory: 28.00 MB
There was 1 error:
1) Tests\Unit\ArticleRepositoryTest::testFetchLatest10Articles
Error: Call to undefined method App\Repositories\ArticleRepository::latest10()
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:52
ERRORS!
Tests: 5, Assertions: 4, Errors: 1.
第三步,只針對紅燈原因修正,並且不應該去修正其他不相關的問題
<?php
namespace App\Repositories;
use App\Models\Article;
class ArticleRepository
{
public function latest10()
{
return Article::query()->orderBy('id', 'desc')->limit(9)->get();
}
}
回到第一步,並重複 1~3 反覆驗證結果直到綠燈(通過測試),流程如下:
紅燈
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
F.... 5 / 5 (100%)
Time: 1.45 seconds, Memory: 28.00 MB
There was 1 failure:
1) Tests\Unit\ArticleRepositoryTest::testFetchLatest10Articles
Failed asserting that 9 matches expected 10.
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:52
FAILURES!
Tests: 5, Assertions: 5, Failures: 1.
修正
<?php
namespace App\Repositories;
use App\Models\Article;
class ArticleRepository
{
public function latest10()
{
return Article::query()->orderBy('id', 'desc')->limit(10)->get();
}
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
..... 5 / 5 (100%)
Time: 1.45 seconds, Memory: 28.00 MB
OK (5 tests, 15 assertions)
ArticleRepositroy 新增 Create Method
先寫測試
ArticleRepositroyTest.php
public function testCreateArticles()
{
$maxId = Article::max('id');
$latestId = ++$maxId;
$article = $this->repository->create([
'title' => 'title ' . $latestId,
'body' => 'body ' . $latestId,
]);
$this->assertEquals($latestId, $article->id);
}
紅燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.E.... 6 / 6 (100%)
Time: 1.76 seconds, Memory: 28.00 MB
There was 1 error:
1) Tests\Unit\ArticleRepositoryTest::testCreateArticles
Error: Call to undefined method App\Repositories\ArticleRepository::create()
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Unit\ArticleRepositoryTest.php:68
重構
ArticleRepositroy.php
public function create($attributes)
{
return Article::create($attributes);
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
...... 6 / 6 (100%)
Time: 1.73 seconds, Memory: 28.00 MB
OK (6 tests, 16 assertions)
Controller
目標: 建立一個取得文章列表的 route “/posts”
對 Controller 行為寫測試 (https://laravel.com/docs/7.x/http-tests)
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ArticleControllerTest extends TestCase
{
public function testArticleList()
{
// 用 GET 方法瀏覽網址 /article
$response = $this->get('/article');
// 改用 Laravel 內建方法
// 實際就是測試是否為 HTTP 200
$response->assertStatus(200);
// 應取得 articles 變數
$response->assertViewHas('articles');
}
}
失敗
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.....F. 7 / 7 (100%)
Time: 1.78 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\ArticleControllerTest::testArticleList
Expected status code 200 but received 404.
Failed asserting that 200 is identical to 404.
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:185
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Feature\ArticleControllerTest.php:16
FAILURES!
Tests: 7, Assertions: 17, Failures: 1.
php artisan make:controller ArticleController --resource
在 routes/web.php 中註冊
Route::resource('article', 'ArticleController');
$ php artisan route:list
+--------+-----------+------------------------+-----------------+------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+------------------------+-----------------+------------------------------------------------+------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | auth:api |
| | GET|HEAD | article | article.index | App\Http\Controllers\ArticleController@index | web |
| | POST | article | article.store | App\Http\Controllers\ArticleController@store | web |
| | GET|HEAD | article/create | article.create | App\Http\Controllers\ArticleController@create | web |
| | GET|HEAD | article/{article} | article.show | App\Http\Controllers\ArticleController@show | web |
| | PUT|PATCH | article/{article} | article.update | App\Http\Controllers\ArticleController@update | web |
| | DELETE | article/{article} | article.destroy | App\Http\Controllers\ArticleController@destroy | web |
| | GET|HEAD | article/{article}/edit | article.edit | App\Http\Controllers\ArticleController@edit | web |
+--------+-----------+------------------------+-----------------+------------------------------------------------+------------+
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.....F. 7 / 7 (100%)
Time: 1.78 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\ArticleControllerTest::testArticleList
The response is not a view.
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:940
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:870
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Feature\ArticleControllerTest.php:19
FAILURES!
Tests: 7, Assertions: 18, Failures: 1.
第一個 assert status 200 通過了,接下來換通過第二個
$response = $this->get('/article');
$response->assertStatus(200);
$response->assertViewHas('articles');
新增 view
cepar@DESKTOP-H3LUMQB MINGW64 ~/Desktop/projects/play-tdd/blog (master)
$ mkdir -p resources/views/articles
cepar@DESKTOP-H3LUMQB MINGW64 ~/Desktop/projects/play-tdd/blog (master)
$ touch resources/views/articles/index.blade.php
controller index method 回傳 view
class ArticleController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$articles = [];
return view('articles.index', compact('articles'));
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
....... 7 / 7 (100%)
Time: 1.77 seconds, Memory: 30.00 MB
OK (7 tests, 18 assertions)
改利用 Service Container (DI) 來自動注入 ArticleRepository
protected $repository;
/**
* ArticleController constructor.
* @param ArticleRepository $repository
*/
public function __construct(ArticleRepository $repository)
{
$this->repository = $repository;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//$articles = [];
$articles = $this->repository->latest10();
return view('articles.index', compact('articles'));
}
紅燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.....F. 7 / 7 (100%)
Time: 2.07 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\ArticleControllerTest::testArticleList
Expected status code 200 but received 500.
Failed asserting that 200 is identical to 500.
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:185
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Feature\ArticleControllerTest.php:17
FAILURES!
Tests: 7, Assertions: 17, Failures: 1.
在測試中印出回傳,看錯誤內容
public function testArticleList()
{
// 用 GET 方法瀏覽網址 /article
$response = $this->get('/article');
dd($response->getContent());
// 改用 Laravel 內建方法
// 實際就是測試是否為 HTTP 200
$response->assertStatus(200);
// 應取得 articles 變數
$response->assertViewHas('articles');
}
缺少資料表
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: articles (SQL: select * from "articles" order by "id" desc limit 10)
由於這邊我們只要測試 controller index 行為是否正常,故不需要用到資料庫,這邊可以利用 mock 物件隔離資料庫做測試
http://docs.mockery.io/en/latest/index.html
https://laravel.com/docs/7.x/mocking#introduction
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Repositories\ArticleRepository;
class ArticleControllerTest extends TestCase
{
public function testArticleList()
{
$this->mock(ArticleRepository::class, function ($mock) {
$mock->shouldReceive('latest10')->once()->andReturn([]);
});
// 用 GET 方法瀏覽網址 /article
$response = $this->get('/article');
// 改用 Laravel 內建方法
// 實際就是測試是否為 HTTP 200
$response->assertStatus(200);
// 應取得 articles 變數
$response->assertViewHas('articles', []);
}
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
....... 7 / 7 (100%)
Time: 1.79 seconds, Memory: 30.00 MB
OK (7 tests, 19 assertions)
建立文章
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// 直接從 Http\Request 取得輸入資料
$this->repository->create($request->all());
// 導向列表頁
return redirect()->action('ArticleController@index');
}
public function testCreateArticleCSRFFailed()
{
$parameters = [];
$response = $this->post('article', $parameters);
$response->assertStatus(419);
}
因為 laravel 中 get 以外的方法有 csrf 保護,所以預期要失敗,但卻得到 200
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
......F. 8 / 8 (100%)
Time: 1.83 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\ArticleControllerTest::testCreateArticleCSRFFailed
Expected status code 419 but received 200.
Failed asserting that 419 is identical to 200.
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:185
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Feature\ArticleControllerTest.php:36
FAILURES!
Tests: 8, Assertions: 20, Failures: 1.
先看看 csrf middleware 註冊在哪邊
// kernel.php
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
接著可以看到測試時不驗證 csrf token
vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Session\TokenMismatchException
*/
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->shouldAddXsrfTokenCookie()) {
$this->addCookieToResponse($request, $response);
}
});
}
throw new TokenMismatchException('CSRF token mismatch.');
}
如果需要模擬驗證 csrf 過程的話,可以加上
public function testCreateArticleCSRFFailed()
{
$this->app['env'] = 'production';
$parameters = [];
$response = $this->post('article', $parameters);
$response->assertStatus(419);
}
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
........ 8 / 8 (100%)
Time: 1.83 seconds, Memory: 30.00 MB
OK (8 tests, 20 assertions)
測試帶 token 通過
public function testCreateArticleWithCSRFSuccess()
{
$this->app['env'] = 'production';
Session::start();
$parameters = [
'title' => 'title 999',
'body' => 'body 999',
'_token' => csrf_token(), // 手動加入 _token
];
$response = $this->post('article', $parameters);
$response->assertStatus(302);
$response->assertRedirect('article');
}
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.......F. 9 / 9 (100%)
Time: 2 seconds, Memory: 32.00 MB
There was 1 failure:
1) Tests\Feature\ArticleControllerTest::testCreateArticleWithCSRFSuccess
Expected status code 302 but received 500.
Failed asserting that 302 is identical to 500.
C:\Users\cepar\Desktop\projects\play-tdd\blog\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:185
C:\Users\cepar\Desktop\projects\play-tdd\blog\tests\Feature\ArticleControllerTest.php:58
FAILURES!
Tests: 9, Assertions: 21, Failures: 1.
補上 mock 物件
public function testCreateArticleWithCSRFSuccess()
{
$this->mock(ArticleRepository::class, function ($mock) {
$mock->shouldReceive('create')->once()->andReturn(true);
});
$this->app['env'] = 'production';
Session::start();
$parameters = [
'title' => 'title 999',
'body' => 'body 999',
'_token' => csrf_token(), // 手動加入 _token
];
$response = $this->post('article', $parameters);
$response->assertStatus(302);
$response->assertRedirect('article');
}
綠燈
$ ./vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
......... 9 / 9 (100%)
Time: 1.9 seconds, Memory: 30.00 MB
OK (9 tests, 24 assertions)
留言
張貼留言