テストコード、ちゃんと書いてる?
こんにちは、かっちゃんです。
今回はデータ取得においてのテストケースにおけるダメな書き方について書いていこうと思います。
環境
- Laravel Framework 6.18.10
- PHP: 7.4.20
前提条件
ルーティングはroutes/api.php のファイルに書いてる事とし、routeは以下の様に設定します。
Route::get('/members', 'MemberController@index')->name('members.index');
今回使用するAPIは以下の様なAPIとします。 DBに保存されている全ての会員の名前、メールアドレス、作成日の3つの項目を古い情報順に取得する簡単なAPIです。
namespace App\Http\Controllers\Api;
use App\Models\Member;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
/**
* 会員取得API
* @package App\Http\Controllers\Api
*/
class MemberController extends Controller
{
/**
* @return JsonResponse
*/
public function index(): JsonResponse
{
$members = Member::orderBy('created_at')->get();
return response()->json(
$members->map(
function (Member $member) {
return [
'name' => $member->name,
'email' => $member->email,
'created_at' => $member->created_at->format('Y-m-d H:i:s')
];
}
)
);
}
}
パターン1
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberGetIndexTest extends TestCase
{
use DatabaseTransactions;
public function testBasicTest(): void
{
$response = $this->get('/api/members');
$response->assertStatus(200);
}
どこのサンプルテストコードだよって感じですよね。
テストの役割としては不十分過ぎます。
情報取得のAPIのテストなのでレスポンスの情報をテストしないと意味がないです。
絶対にやめましょう。
パターン2
namespace Tests\Feature;
use App\Models\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberGetIndexTest extends TestCase
{
use DatabaseTransactions;
public function testGetMembersMatchResponse(): void
{
/** @var Member $member1 */
$member1 = factory(Member::class)->create();
/** @var Member $member2 */
$member2 = factory(Member::class)->create();
$response = $this->get('/api/members');
$response->assertStatus(200);
$response->assertJson(
[
[
'name' => $member1->name,
'email' => $member1->email,
'created_at' => $member1->created_at->format('Y-m-d H:i:s')
],
[
'name' => $member2->name,
'email' => $member2->email,
'created_at' => $member2->created_at->format('Y-m-d H:i:s')
],
]
);
}
}
- テストデータの値が決まっていない
- 変数を用いて期待通りの結果が返って来ているか判定している
テストデータの値が決まっていなければ期待通りの結果が返って来ているのかが分かりませんよね。
しかも変数を使ってテストしてしまっているので、なんのデータが入っているか分からないけど同じデータが返って来ているらしいよ、という風な曖昧な感じになってしまっています。
実装コードと同じ処理を書いた自作自演のようなテストになってしまっていますのでやめましょう。
パターン3
namespace Tests\Feature;
use App\Common\Models\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberGetIndexTest extends TestCase
{
use DatabaseTransactions;
public function testGetMembersMatchResponse(): void
{
factory(Member::class)->create(
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2023-04-28 00:00:00'
]
);
factory(Member::class)->create(
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2023-04-28 00:00:00'
]
);
$response = $this->get('/api/members');
$response->assertStatus(200);
$response->assertJson(
[
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2023-04-28 00:00:00'
],
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2023-04-28 00:00:00'
],
]
);
}
}
- テストのデータが2つとも同じデータになっている
テストデータの値も決まり、レスポンスに対してこの値が返ってくるであろうと言う期待する値を使用できていますね。
ナイスです。
ただこのテストですと初めに作った会員情報と次に作った会員情報の区別がつきません。
古い情報順に会員が取得できているかどうかのテストが行えていません。
これではテストとしての役割を果たせていません。
パターン4
namespace Tests\Feature;
use App\Common\Models\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberGetIndexTest extends TestCase
{
use DatabaseTransactions;
public function testGetMembersMatchResponse(): void
{
factory(Member::class)->create(
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2022-04-28 00:00:00'
]
);
factory(Member::class)->create(
[
'name' => 'テスト花子',
'email' => 'hanako@example.com',
'created_at' => '2023-04-28 00:00:00'
]
);
$response = $this->get('/api/members');
$response->assertStatus(200);
$response->assertJson(
[
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2022-04-28 00:00:00'
],
[
'name' => 'テスト花子',
'email' => 'hanako@example.com',
'created_at' => '2023-04-28 00:00:00'
],
]
);
}
}
- 全件取得出来ているかの保証がされていない
一見これで大丈夫そうに見えるのですが実は $response->assertJson()
に落とし穴があります。
このメソッドはレスポンスと確認するデータが部分一致出来ていれば通ってしまいます。
ですので仮にもう一件会員のデータを増やしたとしてもテストが通ってしまいます。
これですとまだテストに穴がありますので補っていきましょう。
良い例
namespace Tests\Feature;
use App\Common\Models\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberGetIndexTest extends TestCase
{
use DatabaseTransactions;
public function testGetMembersMatchResponse(): void
{
factory(Member::class)->create(
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2022-04-28 00:00:00'
]
);
factory(Member::class)->create(
[
'name' => 'テスト花子',
'email' => 'hanako@example.com',
'created_at' => '2023-04-28 00:00:00'
]
);
$response = $this->get('/api/members');
$response->assertStatus(200);
$response->assertJsonCount(2);
$response->assertJson(
[
[
'name' => 'テスト太郎',
'email' => 'taro@example.com',
'created_at' => '2022-04-28 00:00:00'
],
[
'name' => 'テスト花子',
'email' => 'hanako@example.com',
'created_at' => '2023-04-28 00:00:00'
],
]
);
}
}
このテストコードですとDBに保存されている全ての会員の名前、メールアドレス、作成日の3つの項目を古い情報順に取得取得出来ている事が証明出来ます。
まとめ
テストコードを書くのはとても面倒ですが、バグの早期発見や設計通りにコードを書けているかなどの確認にもなりますので、ちゃんと書いておきましょう。
そういえば、社内の某タカちゃんにはテストコードをテストしないといけない様な分かりにくいテストコードを書くなと言われた事をよく覚えています。
タカちゃん元気かなー、さようなら。