iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
佛心分享-IT 人自學之術

後端小白自學 Laravel系列 第 27

第 27 天:實戰項目 - 後台任務管理應用測試

  • 分享至 

  • xImage
  •  

我是後端的初學者,所以通常會先寫測試讓自己比較熟悉,另外,在開發過程中立即運行測試,也就是完成一個邏輯先跑測試,這樣可以及早發現和修復錯誤,避免在後期修復時增加的工作量和複雜性。

以下主要是依照第 23 天:代碼質量與測試 - 寫測試的流程除了基本規劃以外,進入開發前先寫測試。

目標


今天和明天的任務主要是開後台 api ,主要運用 CRUD 操作來實現後台任務管理應用基本功能

評估


我自己在前端的時候評估任務時程都會拆小的任務包,內容就是確認重要需求以及任務可能會使用的技能,最重要的是...會不會有機會撞牆!?

站在後端角色做一個後台任務管理應用,用到基本的 CRUD ,可以參考前輩的商品後台,另外,站在使用者體驗的角度觀察,如果我是一個後台管理者,我會需要哪些功能?

需要開的 api 明細

no 項目 方法 說明 請求驗證
1 任務列表 GET 顯示所有任務 前端不用帶參數
2 創建任務 POST 新增一個任務 前端依照需求帶參數存入資料庫
3 更新任務 PUT 修改現有的任務 前端路由中帶 task_id
4 更新任務 PETCH 修改現有任務的一小部分 前端路由中帶 task_id 和布林值
5 刪除任務 DELECT 刪除一個任務 前端路由中帶 task_id

Postman 文件建立


過去前端的經驗告訴我:身為後端要做好溝通的橋樑,開發前除了想一下表格架構以外,也可以找雙方討論交流回傳格式,避免前端收到太大的驚喜!

建立文件

前端最討厭的梗圖

圖片來源:(專案倒數1天) 你需要的API我都做完了唷!加油!

建立路由和閉包的方法


第一步會先做建立路由和閉包的方法,主要是要確保給前端的架構和 HTTP 狀態必須符合當初的溝通結果,因為這裡開始就是要回傳給前端的關鍵路徑,其他進入 service 和 repository 的部分就是後端自己的事情,這裡最重要的就是回傳是要正確的,其他後端自己處理即可!

<?php

use App\Http\Controllers\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::prefix('tasks')->name('tasks.')->group(function () {
    Route::post('/', [TaskController::class, 'storeTask'])->name('create');
    Route::put('/{task}', [TaskController::class, 'updateTask'])->name('update');
    Route::patch('/{task}/{completed}', [TaskController::class, 'changeTaskComplete'])->name('change.complete');
    Route::delete('/{task}', [TaskController::class, 'deleteTask'])->name('delete');
    Route::get('/', [TaskController::class, 'showTasks'])->name('show');
});

測試應用的功能 Feature Tests


不過,寫測試之前先做工廠建立,因為要做比對用的!

建立工廠
指令 php artisan make:factory TaskFactory --model=Task,然後在 database/factories/TaskFactory.php 中定義工廠的欄位。

<?php

namespace Database\Factories;

use App\Models\Task;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * 任務工廠
 * 
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Task>
 */
class TaskFactory extends Factory
{
    protected $model = Task::class;

    /**
     * 定義模型的預設狀態。
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'title' => $this->faker->sentence,
            'description' => $this->faker->paragraph,
            'completed' => $this->faker->boolean,
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }
}

寫測試 step by step
首先這裡知道要驗的是 url & controller,所以依照第 23 天:代碼質量與測試 - 寫測試的流程執行。
流程
程式碼測試畫面

<?php

namespace Tests\Feature;

use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * 測試後台任務管理應用控制器
 * 
 */
class TaskControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * 測試取得所有任務明細
     * 
     * @return void
     */
    public function testShowTasks(): void
    {
        // 工廠建資料
        Task::factory()->count(count: 3)->create();

        // 跑結果 & 比對
        $this->get(uri: '/api/tasks')
            ->assertOk()
            ->assertJsonCount(count: 3);
    }

    /**
     * 測試新增任務
     * 
     * @return void
     */
    public function testStoreTask(): void
    {
        $data = [
            "title" => "new_task",
            "description" => "test",
            "completed" => true,
        ];

        $this->post(uri: '/api/tasks', data: $data)
            ->assertStatus(status: 201)
            ->assertJson(value: $data);
        $this->assertDatabaseHas(table: 'tasks', data: $data);
    }

    /**
     * 測試編輯任務
     * @return void
     */
    public function testUpdateTask(): void
    {
        $task = Task::factory()->create();
        $data = [
            'title' => 'Updated Task',
            "description" => "test",
            "completed" => true,
        ];

        $this->put(uri: "/api/tasks/{$task->id}", data: $data)
            ->assertOk()
            ->assertJson(value: $data);

        $this->assertDatabaseHas(table: 'tasks', data: $data);
    }

    /**
     * 測試改變任務的完成欄位
     *
     * @return void
     */
    public function testChangeTaskComplete(): void
    {
        $task = Task::factory()->create(attributes: ['completed' => true]);

        $this->patch(uri: "/api/tasks/{$task->id}/{$task->completed}")
            ->assertOk();
    }

    /**
     * 測試刪除任務
     * 
     * @return void
     */
    public function testDeleteTask(): void
    {
        $task = Task::factory()->create();

        $this->delete("/api/tasks/{$task->id}")
            ->assertOk();
    }
}

單元測試 Unit Tests


單元測試主要分成 service、repository、transformer,甚至是 request 也可以,這邊主要先以 service、repository 為主!

進入單元測試後,一定會很常遇到過不了的問題,因為邏輯只寫了一半,所以難免會需要回頭改,但是至少註解寫上後知道要多注意哪些細節,避免壓線的時候才發現 bug 或是需求沒有完善!開始吧!

service
指令 php artisan make:test TaskServiceTest --unit,預期都是過水,大部分都直接傳入 repository 做 CRUD,所以這裡做一個測試建立任務的邏輯處理

<?php

namespace Tests\Unit;

use App\Models\Task;
use App\Repositories\TaskRepository;
use App\Services\TaskService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Mockery\LegacyMockInterface;
use Mockery\MockInterface;
use Tests\TestCase;

class TaskServiceTest extends TestCase
{
    use RefreshDatabase;

    protected LegacyMockInterface|MockInterface $mock_repository;
    protected TaskService $service;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mock_repository = Mockery::mock(TaskRepository::class);
        $this->service = new TaskService($this->mock_repository);
    }

    /**
     * 測試新增任務的邏輯處理
     * 
     * @return void
     */
    public function testStoreTask(): void
    {
        // 設定測試資料
        $data = [
            'title' => 'New Task',
            'description' => 'Task description',
            'completed' => false,
        ];

        // 用工廠建立預期結果
        $expected = Task::factory()->make($data);

        // 用 mock 開始模擬,調用方法並且執行一次並且回傳前面建立的預設值
        $this->mock_repository->shouldReceive('storeTask')
            ->once()
            ->andReturn($expected);

        // 調用被測試的方法
        $activity = $this->service->storeTask($data);

        // 斷言比對
        $this->assertEquals($expected, $activity);
    }
}

repository
指令 php artisan make:test TaskRepositoryTest --unit

<?php

namespace Tests\Unit;

use App\Models\Task;
use App\Repositories\TaskRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TaskRepositoryTest extends TestCase
{
    use RefreshDatabase;

    protected TaskRepository $repository;

    protected function setUp(): void
    {
        parent::setUp();

        $this->repository = app()->make(TaskRepository::class);
    }

    /**
     * 測試取得所有任務明細的資料處理
     * 
     * @return void
     */
    public function testShowTasks(): void
    {
        Task::factory()->count(3)->create();

        $this->assertDatabaseCount(Task::class, 3);
    }

    /**
     * 測試新增任務的資料處理
     * 
     * @return void
     */
    public function testStoreTask(): void
    {
        // 預設
        $data = [
            'title' => 'New Task',
            'description' => 'Task description',
            'completed' => false,
        ];

        // 模擬跑出來的真值
        $task = $this->repository->storeTask($data);

        // 斷言
        $this->assertDatabaseHas(Task::class, $data);
        $this->assertEquals($data['title'], $task->title);
    }

    /**
     * 測試編輯任務的資料處理
     * 
     * @return void
     */
    public function testUpdateTask(): void
    {
        // 預設
        $task = Task::factory()->create();
        $data = [
            'title' => 'Updated Task',
            'description' => 'Updated description',
            'completed' => true,
        ];

        // 模擬跑出來的真值
        $updatedTask = $this->repository->updateTask($task, $data);

        // 斷言
        $this->assertEquals(expected: $data['title'], actual: $updatedTask->title);
        $this->assertDatabaseHas(Task::class, $data);
    }

    /**
     * 測試改變任務的完成欄位資料處理
     * 
     * @return void
     */
    public function testChangeTaskComplete(): void
    {
        // 預設
        $task = Task::factory()->create(['completed' => false]);

        // 模擬跑出來的真值
        $this->repository->changeTaskComplete($task, true);

        // 斷言
        $this->assertDatabaseHas(table: Task::class, data: ['id' => $task->id, 'completed' => true]);
        $this->assertTrue($task->fresh()->completed);
    }

    /**
     * 測試刪除任務的資料處理
     * 
     * @return void
     */
    public function testDeleteTask(): void
    {
        // 預設
        $task = Task::factory()->create();

        // 模擬跑出來的真值
        $this->repository->deleteTask($task);

        // 斷言
        $this->assertDatabaseMissing(Task::class, ['id' => $task->id]);
    }
}

上一篇
第 26 天:多語言支持
下一篇
第 28 天:實戰項目 - 後台任務管理應用開發
系列文
後端小白自學 Laravel30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言