2023-12-18|閱讀時間 ‧ 約 44 分鐘

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

在前面的篇幅中,與大家分享了許多撰寫 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,

]); } } }}

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

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

本系列文章目錄

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.