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
留言分享你的想法!
avatar-img
WilliamP的沙龍
13會員
495內容數
歡迎來到 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
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
在本節中,我們介紹了PHP的基本語法,包括如何在HTML中嵌入PHP代碼,PHP腳本的執行順序,以及多種註解方式。我們還學習了如何定義和使用變數,包括單個變數和多個變數的賦值方法。這些基礎知識將幫助你開始使用PHP進行Web開發。
Thumbnail
在本節中,我們介紹了PHP的基本語法,包括如何在HTML中嵌入PHP代碼,PHP腳本的執行順序,以及多種註解方式。我們還學習了如何定義和使用變數,包括單個變數和多個變數的賦值方法。這些基礎知識將幫助你開始使用PHP進行Web開發。
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
本文介紹了CSS Battle 17的挑戰題目,並提供瞭解題的技巧和方法。內容包括基本排版、圖形解構能力和技術總結。歡迎大家分享自己的解法與作者交流。
Thumbnail
本文介紹了CSS Battle 17的挑戰題目,並提供瞭解題的技巧和方法。內容包括基本排版、圖形解構能力和技術總結。歡迎大家分享自己的解法與作者交流。
Thumbnail
本篇文章分享了前端工程師在 CSS Battle #166 的解題方式,藉由使用 flex 來進行排版,並提供了相關的技術總結。歡迎一起來學習或練習基礎切版的朋友們和作者互動。
Thumbnail
本篇文章分享了前端工程師在 CSS Battle #166 的解題方式,藉由使用 flex 來進行排版,並提供了相關的技術總結。歡迎一起來學習或練習基礎切版的朋友們和作者互動。
Thumbnail
這篇文章將解析CSS Battle第136題的解法,主要考驗基本CSS置中排版的能力,並提供了作者的解法與技術總結。透過互動學習,讓想要學習或練習基礎切版的朋友可以參與討論。
Thumbnail
這篇文章將解析CSS Battle第136題的解法,主要考驗基本CSS置中排版的能力,並提供了作者的解法與技術總結。透過互動學習,讓想要學習或練習基礎切版的朋友可以參與討論。
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 很多大公司的切版與前端是分開的,但不能因為碰不到就不去理解,假如要系統性的調整樣式,那麼你就一定要懂基礎,就好像你要調整微前端的架構,總不能連包板工具的設定都不會吧! 回到正題,這系列文章每個禮拜三都會更新一題CSS Battle的題
Thumbnail
是的,身為前端工程師的基本功! 還是需要時不時拿出來打磨一番,這系列文章每個禮拜三都會更新一題CSS Battle的題目,歡迎與我交流喔!
Thumbnail
是的,身為前端工程師的基本功! 還是需要時不時拿出來打磨一番,這系列文章每個禮拜三都會更新一題CSS Battle的題目,歡迎與我交流喔!
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 這系列文章每個禮拜三都會更新一題CSS Battle的題目與解法
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 這系列文章每個禮拜三都會更新一題CSS Battle的題目與解法
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 這系列文章每個禮拜三都會更新一題CSS Battle的題目解法
Thumbnail
是的,身為前端工程師的基本功!還是需要時不時拿出來打磨一番! 這系列文章每個禮拜三都會更新一題CSS Battle的題目解法
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News