iT邦幫忙

2021 iThome 鐵人賽

DAY 10
1
Software Development

全端開發包括測試自己一條龍!系列 第 10

Day 10 - Laravel使用Phpunit做單元測試

Introduce

當API規模慢慢擴大,Unit test變得很重要,可以幫助我們檢查原本已經正常的功能,當開發新Feature的時候,可能改寫function,導致我們沒注意到的地方產生錯誤,原本寫好的Unit test就能幫我們找出該錯誤,今天會分別撰寫Controller, Service及Repository的Test,那麼接下來就開始吧.

Repository的Unit test

  1. 建立Repository unit test檔案
$ sail artisan make:test PostRepositoryTest --unit
  1. 確認一下我們準備測試的function內容
  • Function name: createPost()
  • 傳入兩個參數: $title, $content
  • 有使用到auth guard,所以我們需要mock一個假的User
  • 回傳Post model
/**
 * 建立文章
 *
 * @param string $title 標題
 * @param string $content 內文
 * @return mixed
 */
public function createPost(string $title, string $content)
{
    $user = Auth::guard('api')->user();

    $post = new Post();
    $post->title = $title;
    $post->content = $content;
    $post->user_id = $user->id;
    $post->save();

    return $post;
}
  1. 修改phpunit.xml把這兩行原本註解拿掉,我們要另外用另一個資料庫去測試,才不會影響到我們原本資料庫的資料,記得重啟container!
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
  1. 建立User factory用來快速建立User假資料,User model要記得加上use HasFactory
# database/factories/UserFactory
<?php

namespace Database\Factories;

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

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'account' => $this->faker->unique()->safeEmail(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'enabled' => 1,
        ];
    }
}
  1. 修改PostRepositoryTest
  • use RefreshDatabase用來清空資料庫的資料,確保每次測試資料不被資料庫數據影響
  • 在Setup make repository instance供我們後續使用
  • 使用factory快速建立User假資料
  • 把JWTGuard mock起來,這樣我們原本function內用Auth guard去取資料才有東西
  • 開始測試Repository function,傳入參數並驗證資料庫成功建立資料
<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tymon\JWTAuth\JWTGuard;
use Mockery;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use App\Repositories\PostRepository;

class PostRepositoryTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @var PostRepository
     */
    protected $post_repository;

    /**
     * 在每個 test case 開始前執行.
     */
    public function setUp(): void
    {
        parent::setUp();
        $this->post_repository = app()->make(PostRepository::class);
        $this->user = User::factory()->create();
        $this->guard_mock = Mockery::mock(JWTGuard::class);
        Auth::shouldReceive('guard')
            ->with('api')
            ->andReturn($this->guard_mock);
        $this->guard_mock->shouldReceive('user')
            ->andReturn($this->user);
    }

    /**
     * 測試 成功建立文章
     */
    public function testCreatePostShouldSuccess()
    {
        $title = "測試標題";
        $content = "測試內文";
        $this->post_repository->createPost($title, $content);
        $this->assertDatabaseHas('posts', [
            'user_id' => $this->user->id,
            'title' => $title,
            'content' => $content,
        ]);
    }
}


Service的Unit test

  1. 建立Service unit test檔案
$ sail artisan make:test PostServiceTest --unit
  1. 確認一下我們準備測試的function內容
  • Function name: create()
  • 傳入array參數: $data
  • 有使用到repository中的function,我們需把repository mock起來
  • 回傳Post model
/**
 * 建立文章
 * @param array $data
 * @return mixed
 */
public function create(array $data)
{
    $title = Arr::get($data, 'title');
    $content = Arr::get($data, 'content');
    $post = $this->post_repository->createPost($title, $content);

    return $post;
}
  1. 建立Post factory用來快速建立Post假資料,Post model要記得加上use HasFactory
# database/factories/PostFactory
<?php

namespace Database\Factories;

use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title' => $this->faker->word(),
            'content' => $this->faker->word(),
            'user_id' => User::factory()->create()->id,
        ];
    }
}
  1. 修改PostServiceTest
  • 在Setup mock PostRepository
  • 先用factory建立Post model,再來在我們mock的Repository,指定要使用的function name,並指定回傳為Post model,之所以這樣做是因為我們要直接假設Repository執行成功.
  • 設定完mock後,我們Call service function時,內部使用到Repository指定function會回傳我們指定的return
  • 接下來驗證實際回傳符合預期
<?php

namespace Tests\Unit;

use Tests\TestCase;
use Mockery;
use App\Services\PostService;
use App\Repositories\PostRepository;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostServiceTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @var PostRepository
     */
    protected $post_repository_mock;

    /**
     * @var PostService
     */
    protected $post_service;

    /**
     * 在每個 test case 開始前執行.
     */
    public function setUp(): void
    {
        parent::setUp();

        $this->post_repository_mock = Mockery::mock(PostRepository::class);
        $this->post_service = new PostService($this->post_repository_mock);
    }

    /**
     * 測試 建立文章Service處理成功
     */
    public function testCreatePostSuccess()
    {
        $post = Post::factory()->create();
        $fake_input = [
            'title' => '測試標題',
            'content' => '測試內文',
        ];
        $this->post_repository_mock
            ->shouldReceive('createPost')
            ->once()
            ->andReturn($post);
        $actual_result = $this->post_service->create($fake_input);
        $this->assertEquals($post->title, $actual_result['title']);
        $this->assertEquals($post->content, $actual_result['content']);
        $this->assertEquals($post->user_id, $actual_result['user_id']);
    }
}


Controller的Unit test

  1. 到Controller我們要直接針對API做驗證,先建立Controller test檔案
$ sail artisan make:test PostControllerTest
  1. 為API設定name
# routes/api.php
Route::group(['prefix' => 'auth'], function () {
    Route::post('login', [AuthController::class, 'login'])->name('auth.login');
    Route::post('register', [AuthController::class, 'register'])->name('auth.register');
});

Route::middleware(['jwt.auth'])->group(function () {
    Route::group(['prefix' => 'user'], function () {
        Route::get('/', [UserController::class, 'index'])->name('user.index');
    });
    Route::group(['prefix' => 'post'], function () {
        Route::post('/', [PostController::class, 'create'])->name('post.create');
        Route::get('/', [PostController::class, 'index'])->name('post.index');
    });
});
  1. 修改tests/TestCase.php
    • 新增一個function用來產生Token
<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tymon\JWTAuth\Facades\JWTAuth;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    /**
     * 產生 jwt 驗證 token
     *
     * @return mixed
     */
    protected function createToken($user)
    {
        $token = JWTAuth::fromUser($user);

        return 'Bearer '.$token;
    }
}

  1. 修改PostControllerTest
  • 設定Header
  • Setup時,建立User model,並將model傳入我們剛剛設定的createToken()藉此產生token
  • Header當中加入Token
  • 使用Post method call API,API名稱為我們第二步驟所設定的名稱
  • 驗證Response符合預期
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use App\Models\User;
use Tests\TestCase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @var API Header
     */
    protected $header = [
        'X-Requested-With' => 'XMLHttpRequest',
        'Content-Type'     => 'application/json',
    ];

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

        $user = User::factory()->create();
        $this->header["Authorization"] = $this->createToken($user);
    }

    /**
     * 測試 建立文章成功
     */
    public function testCreatePostSuccess()
    {
        $fake_data = [
            'title' => '測試標題',
            'content' => '測試內文',
        ];

        $response = $this->withHeaders($this->header)->postJson(Route('post.create'), $fake_data)->decodeResponseJson();

        $this->assertTrue($response['title'] == $fake_data['title']);
        $this->assertTrue($response['content'] == $fake_data['content']);
    }
}
  1. Run test
$ sail test


上一篇
Day 9 - Laravel 8.0的Error Handling
系列文
全端開發包括測試自己一條龍!10

尚未有邦友留言

立即登入留言