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


<<:  Day13 Random

>>:  快乐很简单,但要简单却很难。

全端入门Day17_前端程序撰写之F12

昨天介绍了CSS,今天就来介绍大家F12的功能, 写网页都要懂得看F12 首先到我们做的index....

Day 30 完赛心得

在开始铁人赛之後才发现这个月不该比铁人赛的 这个月的事情比平常都还要多 虽然硬着头皮写完了30天的文...

Facade 外观模式

在 Structural patterns 当中,最後要来谈的是外观模式。 外观模式提供我们一个简单...

rsync备份操作

现在可以利用前两天建立的ZFS阵列对unRaid 做rsync了~ 会从介绍到实作,根据不同状况进行...

[Day28]简单的contextmenu

职涯在走,铁人赛文章一定要有。 大家好今天我要来继续示范其他的menu,一开始我先来示范contex...