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
8會員
233內容數
歡迎來到 WilliamP 的沙龍天地,在這裡將與各位讀者探討各種主題,包刮高中數學題庫、PHP開發經驗、LINE聊天機器人開發經驗、書摘筆記等,歡迎交流!
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
WilliamP的沙龍 的其他內容
今天我們來聊聊覆蓋率報告吧! 何為覆蓋率報告 & 為何需要覆蓋率報告 所謂的覆蓋率報告,指的是能指出我們的專案程式庫,有被測試程式碼實際測試到的部分的佔比有多少。當我們了解有多少程式碼有被覆蓋到,以及有多少程式碼沒被覆蓋到時,理論上我們可以對程式碼是否可正常運作更有信心! 事前準備 先將以
今天讓我們來看 phpunit.xml 吧! phpunit.xml 位在 Laravel 專案根目錄底下,顧名思義,它是一個設定 PHPUnit 執行方式的設定XML檔,PHPUnit 提供了不少設定值可供設定,這邊只提最重要的幾個: stopOnFailure 說明:當此欄位設定為 tru
今天要來為大家介紹 Storage Mocking 及 HTTP Mocking! Storage Mocking 函數 Storage::fake():當我們希望在執行測試目標行為時,想驗證 Storage 各類行為是否符合預期,但又不要真的增刪改檔案時,可在測試程式碼中呼叫此函數。 Upl
今天來看 Queue Mocking 吧! Queue Mocking 函數 Queue::fake():當我們希望在執行測試目標行為時,想驗證某個 Job 類別是否有被派送至佇列中,但又不要真的觸發 Job 入列時,可在測試程式碼中呼叫此函數。 Queue::assertPushed():可
今天來看 Mail Mocking 吧! Mail Mocking 函數 Mail::fake():當我們希望在執行測試目標行為時, 想驗證是否有觸發到發送 Email ,但又不要真的觸發 Email 的寄送時,可在測試程式碼中呼叫此函數。 Mail::assertSent():可驗證指定的
今天我們會接續環繞著 「Mocking」 這個主題。 在 Laravel 中,有幾個類別的 Mocking 方式,與前兩天所介紹的方式有所不同,在這次的系列文章中,會和大家介紹 Event、Mail、Queue、Storage、HTTP 這幾個類別的特殊 Mocking 方式。 今天就先來看 E
今天我們來聊聊覆蓋率報告吧! 何為覆蓋率報告 & 為何需要覆蓋率報告 所謂的覆蓋率報告,指的是能指出我們的專案程式庫,有被測試程式碼實際測試到的部分的佔比有多少。當我們了解有多少程式碼有被覆蓋到,以及有多少程式碼沒被覆蓋到時,理論上我們可以對程式碼是否可正常運作更有信心! 事前準備 先將以
今天讓我們來看 phpunit.xml 吧! phpunit.xml 位在 Laravel 專案根目錄底下,顧名思義,它是一個設定 PHPUnit 執行方式的設定XML檔,PHPUnit 提供了不少設定值可供設定,這邊只提最重要的幾個: stopOnFailure 說明:當此欄位設定為 tru
今天要來為大家介紹 Storage Mocking 及 HTTP Mocking! Storage Mocking 函數 Storage::fake():當我們希望在執行測試目標行為時,想驗證 Storage 各類行為是否符合預期,但又不要真的增刪改檔案時,可在測試程式碼中呼叫此函數。 Upl
今天來看 Queue Mocking 吧! Queue Mocking 函數 Queue::fake():當我們希望在執行測試目標行為時,想驗證某個 Job 類別是否有被派送至佇列中,但又不要真的觸發 Job 入列時,可在測試程式碼中呼叫此函數。 Queue::assertPushed():可
今天來看 Mail Mocking 吧! Mail Mocking 函數 Mail::fake():當我們希望在執行測試目標行為時, 想驗證是否有觸發到發送 Email ,但又不要真的觸發 Email 的寄送時,可在測試程式碼中呼叫此函數。 Mail::assertSent():可驗證指定的
今天我們會接續環繞著 「Mocking」 這個主題。 在 Laravel 中,有幾個類別的 Mocking 方式,與前兩天所介紹的方式有所不同,在這次的系列文章中,會和大家介紹 Event、Mail、Queue、Storage、HTTP 這幾個類別的特殊 Mocking 方式。 今天就先來看 E
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
練習 PHPUnit 測試的撰寫,依序創建Controller、Service,並針對計算邏輯進行單元測試的練習。
Thumbnail
在 Laravel 中的測試中,PHPUnit 和 Mockery 都可以用來創建測試替身(test double),但它們有不同的方式和功能,以下簡單介紹兩種寫法方式。
前言 基本準備差不多了,也能跑自己的測試,再來就是關於測試腳本的核心:元素定位跟動作,本篇會著重介紹 XPATH 定位的部分
Thumbnail
前言 上篇我們成功執行第一個測試案例,從 Python 腳本透過 Appium 控制模擬器點選設定中的電池,下個問題就是怎麼找元件,這時候就要請出 Appium Inspector 了
前言 經過五個小單元的準備,終於可以開始跑第一個測試了,Appium 本身是個工具,可以搭配各種語言,這邊選擇 Python 作為測試腳本語言,以便之後跟 Robot Framework 串接。
前言 前四篇,把主機作業系統跟待測物準備交代完畢,有需要請自行跳轉取用,接下來就是測試工具的部分,這次測試套件使用大名鼎鼎 Appium 2。 選擇 Appium 2 的理由 歷史悠久:Appium 2012 年公開之後,就廣受測試社群愛戴 站在巨人的肩榜上:架構類似 Selenium的主從式架構,
前言 前幾篇聊到作業系統、Docker 跟 Android 容器的準備,再來就是替 Android 容器開啟 Google Play 套件並安裝待測 App 供後續手動或者自動測試使用。
前言 前兩篇把作業系統跟 Docker 安裝講完了,接下來就是 Android 容器的安裝了,這裡選用 ReDroid ,因為它是開源、高效、又便於管理的方案。
Thumbnail
前言 前篇把 Ubuntu 作業系統的安裝跟準備談完了,有需要可以跳回去看。接下來聊容器服務 Docker 的安裝與使用。 Docker 可以應用的場合很多,這次是會用它來模擬 Android 受測裝置
前言 本 App 自動化測試專題,用來記錄自動化 App 測試的各環節,包含環境準備、套件安裝、腳本編寫、執行測試與整合。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
練習 PHPUnit 測試的撰寫,依序創建Controller、Service,並針對計算邏輯進行單元測試的練習。
Thumbnail
在 Laravel 中的測試中,PHPUnit 和 Mockery 都可以用來創建測試替身(test double),但它們有不同的方式和功能,以下簡單介紹兩種寫法方式。
前言 基本準備差不多了,也能跑自己的測試,再來就是關於測試腳本的核心:元素定位跟動作,本篇會著重介紹 XPATH 定位的部分
Thumbnail
前言 上篇我們成功執行第一個測試案例,從 Python 腳本透過 Appium 控制模擬器點選設定中的電池,下個問題就是怎麼找元件,這時候就要請出 Appium Inspector 了
前言 經過五個小單元的準備,終於可以開始跑第一個測試了,Appium 本身是個工具,可以搭配各種語言,這邊選擇 Python 作為測試腳本語言,以便之後跟 Robot Framework 串接。
前言 前四篇,把主機作業系統跟待測物準備交代完畢,有需要請自行跳轉取用,接下來就是測試工具的部分,這次測試套件使用大名鼎鼎 Appium 2。 選擇 Appium 2 的理由 歷史悠久:Appium 2012 年公開之後,就廣受測試社群愛戴 站在巨人的肩榜上:架構類似 Selenium的主從式架構,
前言 前幾篇聊到作業系統、Docker 跟 Android 容器的準備,再來就是替 Android 容器開啟 Google Play 套件並安裝待測 App 供後續手動或者自動測試使用。
前言 前兩篇把作業系統跟 Docker 安裝講完了,接下來就是 Android 容器的安裝了,這裡選用 ReDroid ,因為它是開源、高效、又便於管理的方案。
Thumbnail
前言 前篇把 Ubuntu 作業系統的安裝跟準備談完了,有需要可以跳回去看。接下來聊容器服務 Docker 的安裝與使用。 Docker 可以應用的場合很多,這次是會用它來模擬 Android 受測裝置
前言 本 App 自動化測試專題,用來記錄自動化 App 測試的各環節,包含環境準備、套件安裝、腳本編寫、執行測試與整合。