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?

Laravel テストのあれこれ

0
Posted at

背景

無事レガシーSierから転職し、現在は自社プロダクト開発を行っております。
そんな現在の現場では、LaravelでTDD開発を行っており、(恥ずかしながらTDDという単語の意味すら知らなかった。)テストコードなどもちろん書いたことなかったため、Laravelのテストコードあれこれを残そうと思いました。
備忘録ですので、悪しからず。
大前提、今参画してる案件での構成に則っています。

1. テストクラスの土台

use Tests\TestCase;                                    // Laravelの機能を使う(DB、HTTP、認証など)
use PHPUnit\Framework\TestCase;                         // 純粋なPHPUnit(Laravel非依存、爆速)

Tests\TestCaseを継承:Laravelアプリがブートされる。DBアクセスやHTTPリクエストのシミュレートができる代わりに遅い。
PHPUnit\Framework\TestCaseを直接継承:Laravel無しで動くロジック(今回でいうと純粋な計算ロジックなど)を検証するとき用。爆速
DBを触るテストにはトレイトを付ける必要あり!!

use Illuminate\Foundation\Testing\DatabaseTransactions; // 各テストをトランザクションで囲み、終わったらロールバック
use Illuminate\Foundation\Testing\RefreshDatabase;       // マイグレーションを毎回re-runする(重いが確実)

現在の職場はDatabaseTransactionsを使う方針。
既存のDBスキーマを使い回して差分だけロールバックするので速い。

2. テストメソッドの命名

職場ではPHPUnit属性+日本語メソッド名というスタイルを採用。

use PHPUnit\Framework\Attributes\Test;

#[Test]
public function saveで本文が更新される(): void
{
    // ...
}

#[Test]属性を付ければメソッド名は自由(test_接頭辞が不要になる)
日本語名にすることで「何を検証しているテストか」がテスト結果一覧にそのまま出る(実行結果でも✓ saveで本文が更新されると表示される)
確かにわかりやすい。

昔ながらの書き方はpublic function test_保存すると本文が更新される()(test_接頭辞で自動認識)。
命名のコツ:「〇〇すると/〇〇の場合、△△になる」という条件→結果の形にすると、テスト一覧がそのまま仕様書になります。
ここら辺が便利。
以前は全部スプシで管理してたからなぁ。。。

3. PHPUnit標準のアサーション(どのテストでも使う)

メソッド 用途 今回の例
assertSame($expected, $actual) 厳密一致(=== $this->assertSame('ほげ', $updated->hoge)
assertEquals() ゆるい一致(型変換あり) あまり推奨しない。曖昧になりがち
assertTrue() / assertFalse() 真偽 $this->assertTrue($validator->fails())
assertNull() / assertNotNull() null判定 $this->assertNull($result)
assertCount($n, $collection) 件数 $this->assertCount(2, $histories)
assertInstanceOf(Class::class, $obj) Interfaceの実装確認に使った
$this->expectException(Class::class) 例外発生の検証 service->save('unknown_key', ...)

assertSameassertEqualsは使い分けが大事。
assertEquals(1, '1')は通ってしまう(型が違っても値が近ければOK)。バグを見逃したくないなら基本assertSameにする!!!!

4. Laravel特有のテストヘルパー

HTTPリクエストのシミュレート

$response = $this->get(route('hoge.hoge-sub.hoge-sub-sub.index'));
$response = $this->post('/hoge/hoge-sub/templates/', ['body' => '...']);
$response->assertStatus(200);
$response->assertRedirect();                    // 何らかのリダイレクト
$response->assertRedirect('/hoge/login');   // 特定先へのリダイレクト
$response->assertSee('期日前リマインド');          // レスポンス本文にテキストが含まれる
$response->assertJson(['success' => true]);

認証系

$this->actingAs($user, 'hoge');   // ログイン済み状態を作る(guard名を第2引数で指定)
$this->assertGuest('hoge');       // 未ログインであることを確認(今回のControllerTestで使用)

DB検証系(今回は使わなかったが頻出)

$this->assertDatabaseHas('hoge', ['key' => 'pre_reminder', 'body' => '新ほげ']);
$this->assertDatabaseMissing('sms_templates', ['key' => 'unknown']);
$this->assertDatabaseCount('sms_template_histories', 2);

これらは今回assertSame($updated->body, ...)のようにモデルを取り直して比較したが、assertDatabaseHasを使えば「DBへ実際に書き込まれているか」をもっと直接的に書ける。

バリデーションルール単体のテスト(FormRequestをHTTP経由せずに検証)

use Illuminate\Support\Facades\Validator;

$rules = (new UpdateHogeRequest())->rules();
$validator = Validator::make(['body' => ''], $rules);
$this->assertTrue($validator->fails());

FormRequestは「HTTPを通さないと動かない」と思われがちだが、rules()は普通のpublicメソッドなので直接呼べます。これで軽量に検証できます。

Mockery(モック)

use Mockery;

$repository = Mockery::mock(SmsTemplateRepositoryInterface::class);
$repository->shouldReceive('findByKey')->with('shared_contact')->andReturn($sharedContact);
$repository->shouldNotReceive('save');  // 呼ばれないことの検証もできる

// tearDown()で必ず Mockery::close(); を呼ぶ(呼ばないと次のテストに影響することがある)
shouldReceive('メソッド名')->with(引数)->once()->andReturn(戻り値) が基本形。
「呼ばれてはいけない」ことを保証したいときはshouldNotReceive()が便利(今回の「許可されないkeyではsave()が呼ばれない」テストで使用)。

5. Playwright(E2E)側のセオリー

test.describe('グループ名', () => {
  test.beforeEach(async ({ page }) => { /* 共通の準備(ログインなど) */ });

  test('振る舞いの説明', async ({ page }) => {
    await expect(page.getByRole('heading', { name: 'ほげ' })).toBeVisible();
  });
});

セレクタはgetByRole優先(page.locator('.card-title')のようなCSSクラス指定より壊れにくい)。理由:クラス名はスタイル都合で変わるが、見出し(role="heading")やボタン(role="button")はユーザー視点で変わりにくい
.filter({ has: ... })で「特定の見出しを含むカード」のように絞り込める
失敗時は自動でスクリーンショット・動画・トレースがtest-results/に残る。原因調査はまずそこを見る。

総括

テストコードを書いたことなかったので初めてみた時は???でした。
今回まとめてみて何となく理解はできた気がする。
DIなども、テストでとても便利な機能だったので、また今度アウトプットしていきたいな。

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?