システム開発事業部の加藤です。
Github Copilot Chat で PHP(Laravel) の Unit テストの作成を試してみました。
前置き
Copilot Chat を利用するにはGitHub Copilotが導入済みであり、
エディタはvscodeである必要があります。
使ってみる
まず、下記のモデルクラスのメソッドについてテストを作成してもらいました。
Sale モデルは自身が公開中かどうかを判定するメソッドを持っています。
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Support\Facades\Date;
8
9class Sale extends Model
10{
11 use HasFactory;
12
13 const CREATED_AT = null;
14 const UPDATED_AT = null;
15
16 /**
17 * 公開中か
18 * @return bool
19 */
20 public function isPublicTerm()
21 {
22 $sale_start_date = Date::parse($this->start_date);
23 $sale_end_date = Date::parse($this->end_date);
24 $now = now();
25
26 // 期間外
27 if ($now->lt($sale_start_date) || $now->gt($sale_end_date)) {
28 return false;
29 }
30
31 return true;
32 }
33}
メニューからテストを作成するを選択すると
同階層のディレクトリ内に テストファイルを作成してくれました。
作成してもらったテストの内容です。
テスト実行用のディレクトリ内にファイルを移動させて実行してみたところ、問題なく通りました。
別の生成方法でも試します。
チャットのメニューを開き、コードを選択した状態で
選択したコードに対してチャットで依頼する形で、テストを作成することもできるようです。
日本語で依頼したところ、コメントの内容も日本語で書いてくれました。
また、チャットで続けて修正を依頼することもできるので、こちらの方が使い勝手が良さそうだなと感じました。
今度はもう少し複雑なコードを試そうと思い、下記に対して同じように作成を依頼してみました。
下記はチケットの予約システムのコードから抜粋したものです。
1/**
2 * キャンセル申し込み
3 */
4class CancelAction
5{
6 /**
7 * @param int $reservation_id
8 * @return void
9 */
10 public function __invoke(int $reservation_id): void
11 {
12 $reservation = Reservation::with(['ticket'])
13 ->whereIn('status', [ReservationStatus::CONFIRMATION->value]) // 予約確認中ステータスのみ対象
14 ->find($reservation_id);
15 if (empty($reservation)) {
16 throw new CancelNotFoundException('対象の予約がありません');
17 }
18
19 $deadline = $reservation->ticket->cancellableDeadline();
20 if ($deadline < now()) {
21 throw new CanNotCancelExeption('キャンセル可能時間を過ぎています');
22 }
23
24 $ticket = Ticket::find($reservation->ticket_id);
25 if (empty($ticket)) {
26 throw new CancelNotFoundException('対象のチケットがありません');
27 }
28
29 $reservation->status = ReservationStatus::CANCEL_CONFIRMED;
30 $reservation->save();
31 }
32}
作成してもらったテストがこちらです。
必須パラメータが足りない等で、factroy でのモデルの生成の仕方に修正が必要でした。
また、対象のファイルからは見えていない独自に定義した関数 (今回でいうとcancellableDeadline) の中身までは、確認してくれないようで、その点は調整が必要でした。
ただ、全ての条件分岐を通過するのに必要なテストケースは網羅できており、雛形の作成としては使えそうかなと思いました。
1class CancelActionTest extends TestCase
2{
3 use RefreshDatabase;
4
5 public function testInvoke_ReservationNotFound_ThrowsException()
6 {
7 $this->expectException(CancelNotFoundException::class);
8
9 $action = new CancelAction();
10 $action(999); // non-existing reservation id
11 }
12
13 public function testInvoke_PastCancellableDeadline_ThrowsException()
14 {
15 $this->expectException(CanNotCancelExeption::class);
16
17 $ticket = Ticket::factory()->create(['cancellable_deadline' => now()->subHour()]);
18 $reservation = Reservation::factory()->create(['ticket_id' => $ticket->id, 'status' => ReservationStatus::CONFIRMATION]);
19
20 $action = new CancelAction();
21 $action($reservation->id);
22 }
23
24 public function testInvoke_TicketNotFound_ThrowsException()
25 {
26 $this->expectException(CancelNotFoundException::class);
27
28 $reservation = Reservation::factory()->create(['ticket_id' => 999, 'status' => ReservationStatus::CONFIRMATION]); // non-existing ticket id
29
30 $action = new CancelAction();
31 $action($reservation->id);
32 }
33
34 public function testInvoke_AllConditionsMet_ChangesReservationStatus()
35 {
36 $ticket = Ticket::factory()->create(['cancellable_deadline' => now()->addHour()]);
37 $reservation = Reservation::factory()->create(['ticket_id' => $ticket->id, 'status' => ReservationStatus::CONFIRMATION]);
38
39 $action = new CancelAction();
40 $action($reservation->id);
41
42 $this->assertDatabaseHas('reservations', [
43 'id' => $reservation->id,
44 'status' => ReservationStatus::CANCEL_CONFIRMED,
45 ]);
46 }
47}
最後に
小さい関数やクラスであるほど、精度は高そうです。
そのまま丸ごと使うことは難しいですが、ゼロから作るよりは時短になるかなと思いました。
余談で、まだ発表段階で利用はできませんが、Github Copilot Workspace はプロジェクト全体からコードを生成してくれるみたいなので、より精度の高いテストが作れそうだなと思いました。