🗂️
【Laravel + PHPUnit】Unit・Featureテストを実装してGitHubActionsでCI/CD環境を構築する話
概要
PHPUnit
を利用したCI/CD
環境の構築までをまとめます
環境
- PHP 7.4.25
- Laravel Framework 6.20.44
Factory
でテストデータを作成する
Factory
クラスを作成
php artisan make:factory UserFactory
Factory
クラスを定義
src/laravel/database/factories/UserFactory.php
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Models\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
});
認証機能の対象のモデルを編集
Authenticatable
クラスを継承します。
後述するテストコードでactingAs()
メソッドを利用した認証時のエラー対策です。
TypeError: Argument 1 passed to Illuminate\Foundation\Testing\TestCase::actingAs() must be an instance of Illuminate\Contracts\Auth\Authenticatable, instance of App\Models\User given
src/laravel/app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use SoftDeletes;
/**
* モデルと関連しているテーブル
*
* @var string
*/
protected $table = 'users';
protected $fillable = [
'name',
'email',
'email_verified_at',
'password',
];
}
Unit
テストを実装
テスト対象クラス
app/Domains/Master.php
class Sample implements Master
{
private int $id;
public const ROLES = [
1 => 'hoge',
2 => 'fuga',
];
public static function getNameById(int $id): string
{
return self::ROLES[$id];
}
public static function getIdByName(string $name): int
{
return array_search($name, self::ROLES);
}
/**
* @throws BadRequestException
*/
public function __construct(int $id)
{
$max = count(self::ROLES);
if ($id < 1) {
throw new BadRequestException('idの値が1以下です');
}
if ($id > $max) {
throw new BadRequestException("idの値が{$max}以上です");
}
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return self::getNameById($this->id);
}
}
テスト
src/laravel/tests/Unit/SampleTest.php
use PHPUnit\Framework\TestCase;
class SampleTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
}
public function test_NG_コンストラクタに引数を渡していない()
{
$this->expectException(\ArgumentCountError::class);
new UserRole();
}
public function test_OK_getId()
{
$id = 1;
$userRole = new UserRole($id);
$this->assertEquals($id, $userRole->getId());
}
public function test_OK_getName()
{
$id = 1;
$name = UserRole::getNameById($id);
$userRole = new UserRole($id);
$this->assertEquals($name, $userRole->getName());
}
}
テスト実行
./vendor/bin/phpunit tests/Unit/
Time: 00:00.145, Memory: 8.00 MB
OK (3 tests, 3 assertions)
Feature
テストを実装
src/laravel/tests/Feature/UserControllerTest.php
<?php
namespace Tests\Feature\Admin\Users;
use App\Http\Middleware\VerifyCsrfToken;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
// テスト実行ごとにDBをリフレッシュ
use RefreshDatabase;
public const INDEX_PATH = 'admin/users';
public const INDEX_VIEW_NAME = 'admin.users.index';
public const SHOW_VIEW_NAME = 'admin.users.show';
public const EDIT_VIEW_NAME = 'admin.users.edit';
protected function setUp(): void
{
parent::setUp();
// テストデータを作成
$this->authUser = factory(User::class)->create();
$this->unAuthUser = factory(User::class)->create();
}
public function test_NG_ログインしていない場合はアクセスできない()
{
$response = $this->get(self::INDEX_PATH);
$response->assertStatus(302);
}
public function test_OK_一覧画面にアクセス()
{
$response = $this->actingAs($this->authUser)->get(self::INDEX_PATH);
$response->assertOk();
$response->assertViewIs(self::INDEX_VIEW_NAME);
}
public function test_OK_検索()
{
$name = User::first()->name;
$path = "admin/users?name=${name}";
$response = $this->actingAs($this->authUser)->get($path);
$response->assertOk();
$response->assertSee($name);
$response->assertViewIs(self::INDEX_VIEW_NAME);
}
public function test_OK_詳細()
{
$id = User::query()->first()->id;
$path = "admin/users/${id}";
$response = $this->actingAs($this->authUser)->get($path);
$response->assertOk();
$response->assertViewIs(self::SHOW_VIEW_NAME);
}
public function test_OK_登録()
{
$path = "admin/users/edit";
$response = $this->actingAs($this->authUser)->get($path);
$response->assertOk();
$response->assertViewIs(self::EDIT_VIEW_NAME);
}
public function test_OK_編集()
{
$id = User::query()->first()->id;
$path = "admin/users/edit/${id}";
$response = $this->actingAs($this->authUser)->get($path);
$response->assertOk();
$response->assertViewIs(self::EDIT_VIEW_NAME);
}
public function test_OK_登録成功()
{
// POST時に419エラーが発生するのでCSRFミドルウェアを無効にする
$this->withoutMiddleware([VerifyCsrfToken::class]);
$path = "admin/users/store";
$response = $this->actingAs($this->authUser)
->post(
$path,
[
"name" => 'テスト',
"email" => "test@example.com",
"password" => 'password',
]
);
$response->assertOk();
}
public function test_OK_更新成功()
{
$this->withoutMiddleware([VerifyCsrfToken::class]);
$path = "admin/users/store";
$response = $this->actingAs($this->authUser)
->post(
$path,
[
"name" => 'テスト',
"email" => "test@example.com",
"password" => 'password',
'base_user_id' => $this->authUser->id,
]
);
$response->assertOk();
}
}
テスト実行
phpdbg -qrr ./vendor/bin/phpunit --coverage-text
Time: 00:00.145, Memory: 8.00 MB
OK (8 tests, 8 assertions)
GitHubActions
にワークフローを定義
.github/workflows/main.yml
name: CodeCheck
on:
push:
branches: [ staging ]
pull_request:
branches: [ staging ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: dokcer-compose up
run: |
# docker-compose build
docker-compose up -d
- name: composer install
run: |
docker-compose exec -T laravel bash -c "composer install"
- name: phpunit
run: |
docker-compose exec -T laravel bash -c "phpdbg -qrr ./vendor/bin/phpunit --coverage-text"
Discussion