PHPUnit 自動化測試大作戰【CH24】

更新 發佈閱讀 25 分鐘

在前面的篇幅中,與大家分享了許多撰寫 PHPUnit 測試程式碼所需的知識,之後的文章就讓我們來來模擬一些情境題,並在這些情境題底下,實際去設計測試案例函數吧!

作為第一個情境題,我們就選「網站文章」來當作第一個挑戰吧!

這邊我們假設網站是採前後端分離的設計,因此我們就專注在測試 API 的部分。

使用案例

  1. 使用者可瀏覽文章清單。
  2. 使用者點選單一文章連結,進入單一文章頁,可觀看到該文章下之評論。
  3. 使用者新增評論後,可看到新的評論立即更新至畫面上。

依據以上的使用案例,我們可規畫出以下 API:

API 規畫

  1. 取得文章清單 GET /api/articles
  2. 取得單一文章內容 GET /api/articles/{id}
  3. 取得單一文章評論清單與內容 GET /api/articles/{id}/comments
  4. 使用者針對指定文章新增評論 POST /api/articles/comments

接著就來實作 API 吧!

API 實作

  • app/Http/Controllers/Api/ApiController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;

use function response;

class ApiController extends Controller{

public function respondJson($data) {

return response()->json([

'data' => $data,

]); } public function respondNotFound() {

return response()->json('', 404);

}}
  • app/Http/Controllers/Api/ArticleController.php
<?php

namespace App\Http\Controllers\Api;

use App\Models\Article;

use App\Models\Comment;

use Illuminate\Http\Request;

use Illuminate\Support\Facades\Auth;

class ArticleController extends ApiController{

public function index(Request $request) {

$articles = Article::all();

return $this->respondJson($articles);

} public function show($id) {

$article = Article::find($id);

if (empty($article)) {

return $this->respondNotFound();

} return $this->respondJson($article);

} public function comments(Request $request, $id) {

$article = Article::find($id);

if (empty($article)) {

return $this->respondNotFound();

} $comments = $article->comments;

return $this->respondJson($comments);

} public function storeComment(Request $request, $id) {

$user = Auth::user();

$article = Article::find($id);

if (empty($article)) {

return $this->respondNotFound();

} $data = [

'article_id' => $article->id,

'content' => $request->input('comment'),

'user_id' => $user->id,

]; $comment = new Comment($data);

$comment->save();

return $this->respondJson($comment);

}}
  • app/Models/Article.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;

class Article extends Model{

use HasFactory;

protected $fillable = [

'content',

]; public function comments() {

return $this->hasMany(Comment::class);

}}
  • app/Models/Comment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model{

use HasFactory;

protected $fillable = [

'content',

'user_id',

'article_id',

]; public function article() {

return $this->belongsTo(Article::class);

}}
  • app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Foundation\Auth\User as Authenticatable;

use Illuminate\Notifications\Notifiable;

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable{

use HasApiTokens, HasFactory, Notifiable;

/** * The attributes that are mass assignable. * * @var array<int, string> */

protected $fillable = [

'name',

'email',

'password',

]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> */

protected $hidden = [

'password',

'remember_token',

]; /** * The attributes that should be cast. * * @var array<string, string> */

protected $casts = [

'email_verified_at' => 'datetime',

];}
  • routes/api.php
<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\Api\ArticleController;

Route::prefix('articles')->group(function() {

Route::get('', [ArticleController::class, 'index'])

->name('article.list');

Route::get('/{id}', [ArticleController::class, 'show'])

->where('id', '[0-9]+')

->name('article.one');

Route::get('/{id}/comments', [ArticleController::class, 'comments'])

->where('id', '[0-9]+')

->name('article.one.comments');

Route::post('/{id}/comments', [ArticleController::class, 'storeComment'])

->middleware('auth:api')

->where('id', '[0-9]+')

->name('article.one.comments.store');

});
  • database/migrations/2014_10_12_000000_create_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;

return new class extends Migration{

/** * Run the migrations. * * @return void */

public function up() {

Schema::create('users', function (Blueprint $table) {

$table->id();

$table->string('name');

$table->string('email')->unique();

$table->timestamp('email_verified_at')->nullable();

$table->string('password');

$table->rememberToken();

$table->timestamps();

}); } /** * Reverse the migrations. * * @return void */

public function down() {

Schema::dropIfExists('users');

}};
  • database/migrations/2022_10_02_174939_create_articles_table.php
<?php

use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;

return new class extends Migration{

/** * Run the migrations. * * @return void */

public function up() {

Schema::create('articles', function (Blueprint $table) {

$table->id();

$table->text('content');

$table->integer('page_vdatabase/migrations/2022_10_02_174939_create_articles_table.phpiews');

$table->timestamps();

}); } /** * Reverse the migrations. * * @return void */

public function down() {

Schema::dropIfExists('articles');

}};
  • database/migrations/2022_10_08_172525_create_comments_table.php
<?php

use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;

return new class extends Migration{

/** * Run the migrations. * * @return void */

public function up() {

Schema::create('comments', function (Blueprint $table) {

$table->id();

$table->integer('user_id');

$table->integer('article_id');

$table->text('content');

$table->timestamps();

}); } /** * Reverse the migrations. * * @return void */

public function down() {

Schema::dropIfExists('comments');

}};

前置準備

這邊我們要準備的是各 Model 的 Factory 類別,以及批次產生測試資料的 Seeders:

  • User Factory
<?php

namespace Database\Factories;

use Illuminate\Support\Str;

use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory{

/** * Define the model's default state. * * @return array */

public function definition(): array {

return [

'name' => $this->faker->name,

'email' => $this->faker->safeEmail,

'email_verified_at' => $this->faker->dateTime(),

'password' => bcrypt($this->faker->password),

'remember_token' => Str::random(10)

]; }}
  • Article Factory
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article> */

class ArticleFactory extends Factory{

/** * Define the model's default state. * * @return array<string, mixed> */

public function definition() {

return [

'content' => $this->faker->text,

'page_views' => 0,

]; }}
  • Comment Factory
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment> */

class CommentFactory extends Factory{

/** * Define the model's default state. * * @return array<string, mixed> */

public function definition() {

return [

'content' => $this->faker->text,

]; }}
  • User Seeder
<?php

namespace Database\Seeders;

use App\Models\User;

use App\Models\UserLog;

use Illuminate\Database\Seeder;

class UserSeeder extends Seeder{

/** * Run the database seeds. * * @return void */

public function run() {

User::factory()

->count(10)

->create();

}}
  • Article Seeder
<?php

namespace Database\Seeders;

use App\Models\Article;

use App\Models\Comment;

use App\Models\User;

use Illuminate\Database\Seeder;

class ArticleSeeder extends Seeder{

/** * Run the database seeds. * * @return void */

public function run() {

$articles = Article::factory()

->count(10)

->create();

$users = User::all();

foreach ($articles as $article) {

$commentCount = random_int(1, 5);

for ($i = 0; $i < $commentCount; $i ++) {

$user = $users->random();

Comment::factory()

->create([

'user_id' => $user->id,

'article_id' => $article->id,

]); } } }}

到這邊為止,我們已經把測試目標準備好了,下一篇我們就來針對各使用案例來寫測試吧!

如果您喜歡這篇文章,歡迎加入追蹤以接收新文章通知 😄

本系列文章目錄

留言
avatar-img
WilliamP的沙龍
16會員
621內容數
歡迎來到 WilliamP 的沙龍天地,在這裡將與各位讀者探討各種主題,包刮高中數學題庫、PHP開發經驗、LINE聊天機器人開發經驗、書摘筆記等,歡迎交流!
WilliamP的沙龍的其他內容
2023/12/18
在前一篇文章中,我們探討了多重資料庫連線情境下,Model 及 Database Assertion 的應對方式,不過實際上筆者認為比較有難度的,其實是 Migration 應對方式。 今天就讓我們來探討這部分吧! Migration 應對方式 對於多重資料庫連線這種情境,筆者實務上做過的對應
2023/12/18
在前一篇文章中,我們探討了多重資料庫連線情境下,Model 及 Database Assertion 的應對方式,不過實際上筆者認為比較有難度的,其實是 Migration 應對方式。 今天就讓我們來探討這部分吧! Migration 應對方式 對於多重資料庫連線這種情境,筆者實務上做過的對應
2023/12/18
今天讓我們探討「缺乏 Migration Files 與 Factory Files」的 Legacy 情境吧! 很多時候我們會遇到沒有 Migration Files 或 Factory Files 的 Legacy Codebase,原因大概有以下幾種: 該程式庫原本不是以 Laravel
2023/12/18
今天讓我們探討「缺乏 Migration Files 與 Factory Files」的 Legacy 情境吧! 很多時候我們會遇到沒有 Migration Files 或 Factory Files 的 Legacy Codebase,原因大概有以下幾種: 該程式庫原本不是以 Laravel
2023/12/18
在實務情境上,常會有在單一專案程式庫中,存取多個不同資料庫的使用情境,在這種情況下,我們通常會設置多個資料庫連線(Database Connection)設定。 在平常開發使用設很方便,但要做測試時就會發現一些問題: 在測試程式碼或 Seeder 中調用 factory() 時,都是在預設連線資
2023/12/18
在實務情境上,常會有在單一專案程式庫中,存取多個不同資料庫的使用情境,在這種情況下,我們通常會設置多個資料庫連線(Database Connection)設定。 在平常開發使用設很方便,但要做測試時就會發現一些問題: 在測試程式碼或 Seeder 中調用 factory() 時,都是在預設連線資
看更多
你可能也想看
Thumbnail
是的,身為前端工程師的基本功! 還是需要時不時拿出來打磨一番,這系列文章每個禮拜三都會更新一題CSS Battle的題目,歡迎與我交流喔!
Thumbnail
是的,身為前端工程師的基本功! 還是需要時不時拿出來打磨一番,這系列文章每個禮拜三都會更新一題CSS Battle的題目,歡迎與我交流喔!
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
本篇文章分享了前端工程師在 CSS Battle #166 的解題方式,藉由使用 flex 來進行排版,並提供了相關的技術總結。歡迎一起來學習或練習基礎切版的朋友們和作者互動。
Thumbnail
本篇文章分享了前端工程師在 CSS Battle #166 的解題方式,藉由使用 flex 來進行排版,並提供了相關的技術總結。歡迎一起來學習或練習基礎切版的朋友們和作者互動。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
在本節中,我們介紹了PHP的基本語法,包括如何在HTML中嵌入PHP代碼,PHP腳本的執行順序,以及多種註解方式。我們還學習了如何定義和使用變數,包括單個變數和多個變數的賦值方法。這些基礎知識將幫助你開始使用PHP進行Web開發。
Thumbnail
在本節中,我們介紹了PHP的基本語法,包括如何在HTML中嵌入PHP代碼,PHP腳本的執行順序,以及多種註解方式。我們還學習了如何定義和使用變數,包括單個變數和多個變數的賦值方法。這些基礎知識將幫助你開始使用PHP進行Web開發。
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
這篇文章將解析CSS Battle第136題的解法,主要考驗基本CSS置中排版的能力,並提供了作者的解法與技術總結。透過互動學習,讓想要學習或練習基礎切版的朋友可以參與討論。
Thumbnail
這篇文章將解析CSS Battle第136題的解法,主要考驗基本CSS置中排版的能力,並提供了作者的解法與技術總結。透過互動學習,讓想要學習或練習基礎切版的朋友可以參與討論。
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
本文介紹了CSS Battle 17的挑戰題目,並提供瞭解題的技巧和方法。內容包括基本排版、圖形解構能力和技術總結。歡迎大家分享自己的解法與作者交流。
Thumbnail
本文介紹了CSS Battle 17的挑戰題目,並提供瞭解題的技巧和方法。內容包括基本排版、圖形解構能力和技術總結。歡迎大家分享自己的解法與作者交流。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News