0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel + PHPUnit】Unit・Featureテストを実装してGitHubActionsでCI/CD環境を構築する話

Last updated at Posted at 2022-03-23

概要

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();

        // POST時に419エラーが発生するのでCSRFミドルウェアを無効にする
        $this->withoutMiddleware([VerifyCsrfToken::class]);

        // テストデータを作成
        $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_登録成功()
    {
        $path = "admin/users/store";
        $response = $this->actingAs($this->authUser)
            ->post(
                $path,
                [
                    "name" => '登録テスト',
                    "email" => "test@example.com",
                    "password" => 'password',
                ]
            );
        $response->assertOk();
        $this->assertDatabaseHas('users', ['name' => '登録テスト']);
    }

    public function test_OK_更新成功()
    {
        $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();
        $this->assertDatabaseHas('users, ['name' => '更新テスト']);
    }
}

テスト実行

./vendor/bin/phpunit tests/feature/

Time: 00:00.145, Memory: 8.00 MB

OK (8 tests, 8 assertions)

GitHubActionsにワークフローを定義

.github/workflows/main.yml
ame: CodeCheck

on:
  pull_request:
    branches: [ main ]

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 "php vendor/bin/phpunit"
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?