🧪

私がテストコードを書くときに気をつけていること〜実例を添えて〜

2022/12/06に公開

はじめに

こんにちは。
皆さんテストは書いていますか?

もはやWebアプリケーションにおいてテストとは空気や水のようになくてはならないものだと思っています。

テストコードがもたらすメリットは数え切れないほどありますが、例を挙げるとアプリケーションの改修を行う上でテストコードがあることによって意図しない変更が加わった場合に気づけたり、テストコードから仕様を理解することができたり、例外が発生した場合のテストを記述することによってインシデントを事前に潰すことができたり、境界値のテストを書くことで網羅性を上げアプリケーションの信頼性を高めたりと、枚挙に暇がありません。

テストの重要性については私が今更語るまでもないとは思いますが、とは言え「重要性はわかるけどどうやってテストを書けばいいの?わからない!」という方もいらっしゃるのではないでしょうか。
過去の私もそうでした。

本記事ではそういった方向けに、段階を踏んで具体的なコードを元にテストコードの書き方を示していきたいと思います。
なお、本記事ではPHP(Laravel)をベースにしており、基本的には単体テスト(PHPUnit)について取り扱います。
E2Eテストなどについては取り扱いません。

段階的に学ぶテストコードの書き方

前提知識

私は最近テストコードを書くにあたって以下の点に注意して書いています。

  1. describe_context_expect を意識したテストケース名にする
  2. テストケースは無理に英語で書かない
  3. 1つのテストケースに対して行うテストは原則1つ
  4. ただしケースが多い場合はループとPHPUnitのオプショナル引数を使用する
  5. AAAを意識する
  6. 責務分割を意識する
  7. 境界値や異常値を意識する
  8. 実行順序を必要とするテストを書かない
  9. 正常系よりも異常系のテストを手厚くする
  10. 複雑な条件が絡む場合はマトリクスでテストケースを洗い出す
  11. コントローラーには一切のビジネスロジックを置かない

それぞれ解説していきます。

describe_context_expect を意識したテストケース名にする

これは何に対してテストを行っているかを明確にする意図があります。
ここのテストケース名が具体的に書けない場合、テスト対象が噛み砕けていない可能性が高いです。
「何のメソッドに対して」「どういった条件でテストして」「結果がどうなる」ということを書きましょう。
イメージ的にはRspec(Ruby on Rails)の describe / context / it を参考にしています。
例えば以下の2つの例を見て、わかりやすいのはどちらでしょうか?

// pattern A.
public function test_sum()
{
    $this->assertTrue(sum(1, 2), 3);
}

// pattern B.
public function test_sum_引数に12を与えた場合_3が返ること()
{
    $this->assertSame(sum(1, 2), 3);
}

テストケースは無理に英語で書かない

これはプロダクトのコーディングルールがある場合はその限りではないのですが、私はテストケースを書く際に context/expect を英語で考えて時間を使ってしまいがちです。
チーム間で合意が取れるのであれば、PHPなどはメソッド名に日本語が使えるため無理に英語にせず、日本語にして認知負荷を下げてもいいのではないでしょうか。
あくまで一個人としての見解です。

1つのテストケースに対して行うテストは原則1つ

describe_context_expect が意識できていればこちらは自然とクリアできるかと思いますが、例えば以下の例だとどこでテストが落ちているかパッとわからないため特定・修正が難しくなります。

public function test_sum()
{
    $result1 = sum(1, 2);
    $this->assertSame($result1, 3);

    $result2 = sum(-1, 2);
    $this->assertSame($result2, 1);

    $result3 = sum(100, 2);
    $this->assertSame($result3, 100);
}

ただしケースが多い場合はループとPHPUnitのオプショナル引数を使用する

とは言え引数が多かったり、似たようなパターンが散見されたりする場合は、期待される値を配列に入れてループで回すという手法を取ることもあります。
ただ、そのままだとどのケースで落ちるかわからないため、オプショナル引数の $message にcontextを入れるようにすると可視性が上がります。

ドキュメント

ref: https://phpunit.readthedocs.io/ja/latest/assertions.html#assertsame

assertSame(mixed $expected, mixed $actual[, string $message = ''])

単純にループを回した場合

public function test_sum()
{
    $rows = [
        ['expect' => 3, 'value1' => 1, 'value2' => 2],
        ['expect' => 1, 'value1' => -1, 'value2' => 2],
        ['expect' => 1, 'value1' => 1, 'value2' => 2],
    ];
    
    foreach ($rows as $row) {
        $this->assertSame($row['expect'], sum($row['value1'], $row['value2']));
    }
}
F                                                                   1 / 1 (100%)

Time: 00:01.339, Memory: 24.00 MB

There was 1 failure:

Failed asserting that 3 is identical to 1.

messageを指定した場合

public function test_sum()
{
    $rows = [
        ['expect' => 3, 'value1' => 1, 'value2' => 2, 'context' => '1 + 2 = 3'],
        ['expect' => 1, 'value1' => -1, 'value2' => 2, 'context' => '-1 + 2 = 1'],
        ['expect' => 1, 'value1' => 1, 'value2' => 2, 'context' => '1 + 2 = 3'],
    ];

    foreach ($rows as $row) {
        $this->assertSame($row['expect'], sum($row['value1'], $row['value2']), $row['context']);
    }
}
F                                                                   1 / 1 (100%)

Time: 00:00.932, Memory: 24.00 MB

There was 1 failure:

1 + 2 = 3
Failed asserting that 3 is identical to 1.

AAAを意識する

AAA(arrange, act, assert) を意識したコードを書きましょう。
要はテストケース内の処理を「テストデータを準備して」「テスト対象コードを実行して」「結果を検証する」というフェーズに分けましょう、というものです。
詳しくは「AAA を意識して単体テストを書く」に記載されていますので、こちらをご覧いただければと思います。

責務分割を意識する

よくありがちなのが、メソッドの中で色々なクラスの処理を呼んでいるケースです。
以下の処理をしている場合にするべきテストはどこまででしょうか?

class UserPostUseCase
{
    public function execute()
    {
        // A. DBから処理レコードを取得する
	// B. レコードを元にAPIを叩く
	// C. レスポンスを元にレコードをINSERT
	// D. 別クラスの処理を実行
	// E. 処理結果を整形して返却
    }
}

この場合、私はA/C/Eをテストして、B/Dはクラスごとモックしてそれぞれのクラスでテストを書きます。
モックとは簡単に言えば本物のクラスを呼び出すのではなく擬似的なクラスとして差し替えて「処理を成功した(あるいは失敗した)」ように振る舞うものです。
モックの具体的な方法については後述します。

テストに対するコストは一般的に結合テストに近づけば近づくほど高くなるとされていて、LaravelなどのMVC2フレームワークで言うと View > Controller > Model > その他ビジネスロジック というような構図になると思います。
経験則、テスト容易性を上げることによって結果的に保守性の高いアプリケーションコードになっていくと思っているので、この辺りのアプリケーション/クラスが持つ責務を明確にした上で適切に分離することは常に意識しています。

境界値や異常値を意識する

テストケースを書く上で境界値は必ず気にするようにしています。
例えば日付を意識したテストの場合、 2022-12-31 23:59:592023-01-01 00:00:00 で処理が変わる場合、どちらもテストを書くようにしています。
あるいはうるう年なども意図しない挙動が混入しやすいものになるので、場合によってはケースを追加しています。
それ以外にも、異常値(例えば割り算を行うメソッドで0が入ってきた場合など)についてもケースを追加しています。
これらの値を適切に認識してテストを書くことで、より安心感のあるアプリケーションになることでしょう。

実行順序を必要とするテストを書かない

特にデータベーステストなどで顕著なのですが、レコードを作成することによって複数のテストケースを実行した際に結果が変わるものなどがあります。
ここに対する観点が漏れているとCIでテストした場合にフレーキーな挙動(※)をするなど、思わぬところで罠にハマる危険性をはらんでいます。
Laravelであれば RefreshDatabase トレイトなど便利な機能があるので、そういったものを使いつつテストケースが他のテストケースに依存しないような書き方を心がけています。

※例えば10回に1回だけテストが落ちるとか

正常系よりも異常系のテストを手厚くする

正常系のテストについてはユニットテスト以外にも自分でブラウザを触って検証すると思われるので意図しない挙動をした場合に検知しやすいのですが、異常系や例外処理はそもそも起こしにくいためカバーできていない事が多いです。
なので私は正常系のテストは当然やりますが、異常系テストについてのテストケースをより手厚く行うようにしています。

複雑な条件が絡む場合はマトリクスでテストケースを洗い出す

例えば以下の要件があったとして、網羅的にテストケースを洗い出すことは可能でしょうか?

ユーザーは自分のアカウントを編集することができるが、他人のアカウントは編集できない。
ただし特権フラグがONの場合はその限りではなく、他人のアカウントを編集することができる。
管理者はより強い権限を有するため、特権フラグがなくとも他人のアカウントを編集できる。
また不正利用があった場合にアカウントを無効化することが可能であるが、無効化できる権限を有するのは特権フラグを有する管理者かメンバーのみに限定される。
再有効化をすることが可能なのは更に限定され、特権フラグを付与された管理者のみである。

テキストで書くと何がなんだかよくわかりませんね。
一旦「権限」「特権フラグの有無」「処理」でマトリクスに落とし込んでみましょう。

メンバー

処理 特権フラグON 特権フラグOFF
自分のアカウント編集
他人のアカウント編集
アカウント有効化
アカウント無効化

管理者

処理 特権フラグON 特権フラグOFF
自分のアカウント編集
他人のアカウント編集
アカウント有効化
アカウント無効化

かなりスッキリしたので、これを元にテストコードを書くことができそうですね!
念のためコードも掲載しておきます。

アプリケーションコード
class AccountManager
{
    protected User $user;
    
    public function __construct(User $user)
    {
        $this->user = $user;
    }
    
    public function canManagedAccount(): bool
    {
        if ($this->user->isAdmin()) {
	    return true;
	}
	return $this->user->hasPrivilege();
    }

    public function canActivateAccount(): bool
    {
	return $this->user->hasPrivilege();
    }

    public function canInactivateAccount(): bool
    {
	return $this->user->isAdmin() && $this->hasPrivilege();
    }
}
テストコード
class AccountManagerTest extends TestCase
{
    public function test_正しく権限が付与されること()
    {
        $rows = [
	    [
	        'role'      => 'member',
		'privilege' => false,
		'expects'   => [
		    'canManagedAccount'    => false,
		    'canActivateAccount'   => false,
		    'canInactivateAccount' => false,
		],
	    ],
	    [
	        'role'      => 'member',
		'privilege' => true,
		'expects'   => [
		    'canManagedAccount'    => true,
		    'canActivateAccount'   => true,
		    'canInactivateAccount' => false,
		],
	    ],
	    [
	        'role'      => 'admin',
		'privilege' => false,
		'expects'   => [
		    'canManagedAccount'    => true,
		    'canActivateAccount'   => false,
		    'canInactivateAccount' => false,
		],
	    ],
	    [
	        'role'      => 'admin',
		'privilege' => true,
		'expects'   => [
		    'canManagedAccount'    => true,
		    'canActivateAccount'   => true,
		    'canInactivateAccount' => true,
		],
	    ],
	];
	
	foreach ($rows as $row) {
	    $user = User::factory()->create(['role' => $row['role'], 'privilege' => $row['privilege']]);
	    foreach ($row['expects'] as $method => $expect) {
	        $result = $user->$method();
		$context = "$method will return " . var_exort($expect, true) . '.';
		
		$this->assertSame($expect, $result, $context);
	    }
	}
    }
}

これはまだケースが少ないため把握できますが、組み合わせが肥大化した場合は一旦パターンを整理して網羅したものをテストコードに落とし込むなどのアプローチが取れるといいかなと思います。

コントローラーには一切のビジネスロジックを置かない

テストに対するコストは一般的に結合テストに近づけば近づくほど高くなるとされていて、LaravelなどのMVC2フレームワークで言うと View > Controller > Model > その他ビジネスロジック というような構図になると思います。

にも書いたとおりで、コントローラーにビジネスロジックを置いてしまうとそれだけでモックがしづらくなってしまってテスト容易性が極端に下がります。
この課題に対しての有効なアプローチとして、様々な記事を参考に「5年間 Laravel を使って辿り着いた,全然頑張らない「なんちゃってクリーンアーキテクチャ」という落としどころ」に行き着いて感銘を受けましたので、よろしければ合わせてご一読いただければと思います。
要はコントローラーの処理をまとめてユースケースクラスなどに切り出してしまい、テストはユースケースに対して行い、コントローラーからはユースケースを呼ぶだけにするという構成にするというものです。
かなり保守性が上がるのでオススメです。


テストの書き方

  • 最も簡単なテスト
  • データベースに対するテスト
  • APIをコールしているメソッドに対するテスト
  • 例外処理を意識したテスト
  • ログに対するテスト
  • 日付が絡むテスト

最も簡単なテスト

アプリケーションコード

class Calc
{
    public function sum(int $val1, int $val2): int
    {
        return $val1 + $val2;
    }
}

テストコード

class CalcTest extends TestCase
{
    public function test_sum_1と2を与えた場合_3が返ること()
    {
        $class = new Calc();
        $this->assertSame(3, $calc->sum(1, 2));
    }
}

この辺りは調べたらすぐ出てくる内容ですね。

データベースに対するテスト

データベースに対するテストとして、Modelを用いたCRUDやクエリビルダーを用いたRead系のテストなどがあるかと思います。
前者に対してはLaravelのモデルファクトリが提供されているため、そちらを使いつつテストを書いていきましょう。
後者については私は「クエリビルダーが生成するクエリが意図したものであること」及び「拾われるべきレコード、拾われるべきではないレコードなど複数パターンデータを用意した上で意図したものだけピックアップされること」は最低限テストするようにしています。
ともすれば今自分が書いているテストがアプリケーションコードに対するものであるのか、フレームワークに対するものであるのかわからなくなるため、後者に対するテストにならないよう気をつけています。

以下に例を示します。

Modelを用いたCRUD

アプリケーションコード
class User extends Model
{
    public function updateOrCreateActiveUser(int|null $id, array $params): bool
    {
        $user = $this->firstOrNew(['id' => $id]);
	$params = array_merge($params, ['status' => 'active']);
	return $user->fill($params)->saveOrFail();
    }
}
テストコード
class UserTest extends TestCase
{
    public function test_updateOrCreateActiveUser_アカウントが存在する場合_レコードが更新されること()
    {
        // Arrange
	$user = User::factory()->create(['status' => 'inactive']);
	$params = ['email' => $expectMail = 'test@example.com'];
	
	// Act
	$result = (new User())->updateOrCreateActiveUser($user->id, $params);
	
	// Assert
	$this->assertSame('active', $result->status);
	$this->assertSame($expectMail, $result->email);
    }
    
    public function test_updateOrCreateActiveUser_アカウントが存在しない場合_レコードが作成されること()
    {
        // Arrange
	$user = User::factory()->create(['status' => 'inactive']);
	$params = ['email' => $expectMail = 'new@example.com'];
	
	// Act
	$result = (new User())->updateOrCreateActiveUser(null, $params);
	
	// Assert
	$this->assertSame('inactive', $user->status);
	$this->assertNotSame($user->id, $result->id);
	$this->assertSame('active', $result->status);
	$this->assertSame($expectMail, $result->email);
    }
}

クエリビルダーに対するテスト

アプリケーションコード
class ActiveUsersEmailSelectQueryBuilder
{
    public function execute(): \Illuminate\Database\Eloquent\Builder
    {
        \DB::table('users')
	    ->selectRaw('email, count(*) as total')
	    ->where(['status' => 'active'])
	    ->groupBy('email')
	    ->having('total', '>', 1);
    }
}
テストコード
class ActiveUsersEmailSelectQueryBuilderTest extends TestCase
{
    public function test_execute_想定したクエリが生成されること()
    {
        // Act
	$result = (new ActiveUsersEmailSelectQueryBuilder())->execute()->toSql();
	$expect = 'select email, count(*) as total from `users` where (`status` = ?) group by `email` having `total` > ?';
	
	// Assert
	$this->assertSame($expect, $result);
    }
    
    public function test_execute_意図したレコードが抽出されること()
    {
        // Arrange
	$emailToBeExtracted = 'extract@example.com';
	$emailNotToBeExtracted = 'not-extract@example.com';
	User::factory()->create(['status' => 'inactive', 'email' => $emailToBeExtracted]);
	User::factory()->create(['status' => 'active', 'email' => $emailToBeExtracted]);
	User::factory()->create(['status' => 'active', 'email' => $emailToBeExtracted]);
	User::factory()->create(['status' => 'active', 'email' => $emailNotToBeExtracted]);

	// Act
	$result = (new ActiveUsersEmailSelectQueryBuilder())->execute();
	
	// Assert
	$this->assertSame(1, $result->count());
	$result = $result->first();
	$this->assertSame($emailToBeExtracted, $result->email);
	$this->assertSame(2, $result->total);
    }    
}

APIをコールしているメソッドに対するテスト

PHPにはMockeryと呼ばれる強力なモックオブジェクトフレームワークが存在します。
モックを使うとどういったメリットがあるのでしょうか?具体例を交えながら考えてみましょう。

アプリケーションコード

class User extends Model
{
    public function postSomething(string $message): bool
    {
        $client = new SomethingClient($this->token);
        $result = $client->post($message);
	return $result['status'] == 200;
    }
}

上記例で言えば postSomething を呼ぶとAPIが実際にコールされるため、テストする上で不都合が生じます。
テストを実行するたびに毎回Postリクエストが飛ぶため、望まない挙動をしたり、そもそもテスト環境で動作しなかったり、あるいはAPI側のレートリミットに引っかかってテスト結果がフレーキー(不安定)になったりといろいろな不都合が生じます。

ここで SomethingClient をモックすることで、「APIをコールした」ことにして処理を進めることができます。
ただ、今のままでは User クラス内部で SomethingClient がインスタンス化されており、密結合であると言えます。
これではテストを書きにくいため、少しだけアプリケーションコードに手を加えます。
具体的には、外部からインスタンスをインジェクションするように手を加え、モック化しやすくするための改修です。
setClient がセッターとして機能しますが、これがテストのためのモック用メソッドです。

以下に例を示します。

アプリケーションコード
class User extends Model
{
    protected SomethingClient $client;
    
    public function __contract()
    {
        $this->client = new SomethingClient();
    }
    
    public function postSomething(string $message): bool
    {
        $result = $this->client->post($message);
	return $result['status'] == 200;
    }
    
    public function setClient(SomethingClient $client)
    {
        $this->client = $client;
    }
}
テストコード
class User extends TestCase
{
    public function test_postSomething_postに成功した場合_trueが返ること()
    {
        // Arrange
	$user = User::factory()->create(['token' => $token = uniqid('token-')]);
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('post')
	    // 意図した引数が渡ることをテスト
	    ->with($token)
	    ->andReturn(['status' => 200]);
	    
	// Act
	$user->setClient($clientMock);
	$result = $user->postSomething('test message');
	
	// Assert
	$this->assertTrue($result);
    }

    public function test_postSomething_postに失敗した場合_falseが返ること()
    {
        // Arrange
	$user = User::factory()->create(['token' => $token = uniqid('token-')]);
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('post')
	    // 意図した引数が渡ることをテスト
	    ->with($token)
	    ->andReturn(['status' => 500]);
	    
	// Act
	$user->setClient($clientMock);
	$result = $user->postSomething('test message');
	
	// Assert
	$this->assertFalse($result);
    }
}

Q. protectedなメソッドに対してモックしたいんですが…
A. protectedメソッドのモックを読んでください

例外処理を意識したテスト

ここまでで正常系のテストは網羅してきましたが、異常系のテストについてはまだパターンが足りていないことと思います。
以下の例を参考に、テストコードを書いていきましょう。

アプリケーションコード
class UpdateAndFetchUserProfileUseCase
{
    protected array $params;
    protected User $user;
    protected SomethingClient $client;
    
    public function __construct(User $user, array $params)
    {
        $this->user = $user;
	$this->params = $params;
	$this->client = new SomethingClient();
    }
    
    public function setClient(SomethingClient $client)
    {
        $this->client = $client;
    }
    
    public function execute()
    {
        $result = $this->client->fetchUserStatus($this->user->account_id, $this->user->token);
	if ($result['status'] != '200') {
	    throw new Exception('failed fetchUserStatus.');
	}
	
	try {
            DB::beginTransaction();
	    $this->user->fill(['status' => $result['account']['status']])->saveOrFail();
	    $result = $this->client->updateUserProfile($this->params);
	    if ($result['status'] != '204') {
	        throw new Exception('failed updateUserProfile.');
	    }
	    DB::commit();
	    return $result['account'];
	} catch (Throwable $e) {
	    DB::rollBack();
	    throw $e;
	}
    }
}
テストコード
class UpdateAndFetchUserProfileUseCaseTest extends TestCase
{
    public function test_execute_fetchUserStatusに失敗した場合_エラーがスローされレコードが更新されないこと()
    {
	$user = User::factory()->create([
	    'account_id' => $accountId = 'test', 
	    'token' => $token = uniqid('token-'), 
	    'status' => 'active',
	]);
	$params = ['icon' => 'test.png'];
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('fetchUserStatus')
	    ->with($accountId, $token)
	    ->andReturn(['status' => 404]);
	    
	$usecase = new UpdateAndFetchUserProfileUseCase($user, $params);
	$usecase->setClient($clientMock);
	$this->expectException(Exception::class);
        $this->expectExceptionMessage('failed fetchUserStatus.');
	
	$usecase->execute();
	
	$user = User::find($user->id);
	$this->assertSame('active', $user->status);
    }

    public function test_execute_アカウント更新に失敗した場合_エラーがスローされレコードが更新されないこと()
    {
	$user = User::factory()->create([
	    'account_id' => $accountId = 'test', 
	    'token' => $token = uniqid('token-'), 
	    'status' => 'active',
	]);
	$params = ['icon' => 'test.png'];
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('fetchUserStatus')
	    ->with($accountId, $token)
	    ->andReturn(['status' => 200, 'account' => ['status' => 'inactive']]);
	$userMock = \Mockery::mock(User::class)->makePartial();
        $userMock->shouldReceive('saveOrFail')
	    ->with($params)
	    ->andThrow(Exception::class, 'error');
	    
	$usecase = new UpdateAndFetchUserProfileUseCase($userMock, $params);
	$usecase->setClient($clientMock);
	$this->expectException(Exception::class);
        $this->expectExceptionMessage('error');
	
	$usecase->execute();
	
	$user = User::find($user->id);
	$this->assertSame('active', $user->status);
    }

    public function test_execute_プロフィール更新に失敗した場合_エラーがスローされレコードが更新されないこと()
    {
	$user = User::factory()->create([
	    'account_id' => $accountId = 'test', 
	    'token' => $token = uniqid('token-'), 
	    'status' => 'active',
	]);
	$params = ['icon' => 'test.png'];
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('fetchUserStatus')
	    ->with($accountId, $token)
	    ->andReturn(['status' => 200, 'account' => ['status' => 'inactive']]);
        $clientMock->shouldReceive('updateUserProfile')
	    ->with($params)
	    ->andReturn(['status' => 500]);
	    
	$usecase = new UpdateAndFetchUserProfileUseCase($user, $params);
	$usecase->setClient($clientMock);
	$this->expectException(Exception::class);
        $this->expectExceptionMessage('failed updateUserProfile.');
	
	$usecase->execute();
	
	$user = User::find($user->id);
	$this->assertSame('active', $user->status);
    }

    public function test_execute_プロフィール更新に成功した場合_レコードが更新されること()
    {
	$user = User::factory()->create([
	    'account_id' => $accountId = 'test', 
	    'token' => $token = uniqid('token-'), 
	    'status' => 'active',
	]);
	$params = ['icon' => 'test.png'];
	$clientMock = \Mockery::mock(SomethingClient::class)->makePartial();
        $clientMock->shouldReceive('fetchUserStatus')
	    ->with($accountId, $token)
	    ->andReturn(['status' => 200, 'account' => ['status' => 'inactive']]);
        $clientMock->shouldReceive('updateUserProfile')
	    ->with($params)
	    ->andReturn([
	        'status' => 204, 
		'account' => $expect = [
		    'status' => 'inactive', 
		    'icon' => 'test.png',
                ]
	    ]);
	    
	$usecase = new UpdateAndFetchUserProfileUseCase($user, $params);
	$user->setClient($clientMock);	
	$result = $usecase->execute();
	
	$user = User::find($user->id);
	$this->assertSame('inactive', $user->status);
	$this->assertSame($expect, $result);
    }
}

依存するインスタンスは全てインジェクションできるようにしており、処理中に任意の振る舞いを取れるようにしてあります。

日付が絡むテスト

最後に、日付によって動作が変わるテストの書き方を紹介します。

アプリケーションコード
class User
{
    public function isTrial()
    {
        return $this->created_at->addDay(10)->gte(Carbon::now());
    }
}
テストコード
class User extends TestCase
{
    public function test_isTrial_アカウント作成が11日前_falseが返ること()
    {
	$this->trabelTo(Carbon::subDay(11));
	$user = User::factory()->create();
	$this->trabelBack();
	
	$this->assertFalse($user->isTrial());
    }
    
    public function test_isTrial_アカウント作成が10日前_trueが返ること()
    {
	$this->trabelTo(Carbon::subDay(10));
	$user = User::factory()->create();
	$this->trabelBack();
	
	$this->assertTrue($user->isTrial());
    }

    public function test_isTrial_アカウント作成が今日_trueが返ること()
    {
	$user = User::factory()->create();
	
	$this->assertTrue($user->isTrial());
    }
}    

終わりに

ちなみにありがちなパターンとして、「テストコードのない既存のアプリケーションコードに対してアプローチしていく」というものがあるかと思います。
そのパターンについては「テストコードのないアプリケーションコードとの向き合い方」に詳しく記載しておりますのでこちらもご覧いただければ幸いです。
私が思うに、テストコードを書かないことによって短期的に開発の速度を上げることはできますが、それはドーピングのようなもので、長期的(と言っても案外短くて3ヶ月程度で現れる)なスパンで見ると思わぬ不具合やエンバグの元となることは間違いないと思います。
確かに短期的に見ると学習コストが発生したり、実装工数が増えたりと言ったマイナス面もあるかもしれません。
しかしながら冒頭にも述べたとおり、テストコードがもたらす恩恵は非常に多岐に渡るため、ぜひ皆さんもテストと仲良くしていただければこんなに嬉しいことはありません。

Discussion