AIにテストコードを書かせてみた
はじめに
担当している案件にて、AIにテストコードを書かせてエンジニアの負担を減らそうという動きがありました。
しかし、ただ何も考えずにAIに任せっきりになってしまうと本筋とはかけ離れたテストコードが出来上がってしまい、テストカバレッジが低くなるといった問題があります。
そこでこの記事では、実際の案件に携わる中で自分が感じた、
AIを使って質の高いテストコードを書くためのポイントをまとめて紹介したいと思います。
採用AIについて
今回はgithub copilotを使用してテストコードを作成しました。
理由としては主に以下のメリットがあると感じたからです
- 使用しているIDE(VSCode)に統合されているため、AIがコードを直接編集でき、コピー&ペーストの手間が省ける。
- 記述しているコードの現在の状況、前後の文脈、開いているファイル全体を理解し、次に書きたいコードを予測してリアルタイムで提案できる
- ワンクリックで実装中のファイルを参照し、それに合ったコードを自動生成してくれる。
実際に使用してみる
テストコードをAIに生成してもらう際、どのようなプロンプトを投げるかでコードの質が大きく変わると感じました。
今回は試しに以下の関数のテストコードを作成してみようと思います
export const add = (a: number, b: number) => a + b;
まず、ほぼ指示を与えずにプロンプトを作成してみます
この関数のテストコードを書いてください。
export const add = (a: number, b: number) => a + b;
その結果、以下のようなテストコードが生成されました
import { add } from './testcode';
describe('add function', () => {
test('should add two positive numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
test('should add two negative numbers correctly', () => {
expect(add(-2, -3)).toBe(-5);
});
test('should add positive and negative numbers correctly', () => {
expect(add(5, -3)).toBe(2);
expect(add(-5, 3)).toBe(-2);
});
test('should handle zero correctly', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
expect(add(0, 0)).toBe(0);
});
test('should handle decimal numbers correctly', () => {
expect(add(1.5, 2.5)).toBe(4);
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
こちらでも十分テストコードとしては成り立っていますが、以下の点に着目すると、まだ所々甘いところがあると思います。
- 正常系しか書かない
- 境界値が弱い
- テストを作成した意図がわかりにくい
なので、次に以下のように詳しく指示を与えてみようと思います
以下の関数のテストコードをJestで書いてください。
export const add = (a: number, b: number) => a + b;
要件:
- 正常系だけでなく例外系テストも含める(もし仕様上あり得る場合)
- 境界値テストも含める
- Jest v29を使用している想定
- import文も含めて書く
- テストケースごとに「なぜそのケースが必要か」のコメントを入れる
その結果、次のようなテストコードが作成されました
import { add } from './testcode';
describe('add function', () => {
// 正常系: 基本的な足し算の動作確認
test('should add two positive numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
// 正常系: 負の数の足し算が正しく動作することを確認
test('should add negative numbers correctly', () => {
expect(add(-2, -3)).toBe(-5);
});
// 正常系: 正の数と負の数の足し算が正しく動作することを確認
test('should add positive and negative numbers correctly', () => {
expect(add(5, -3)).toBe(2);
expect(add(-5, 3)).toBe(-2);
});
// 境界値テスト: ゼロとの足し算が恒等元として機能することを確認
test('should handle zero as identity element', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
expect(add(0, 0)).toBe(0);
});
// 境界値テスト: 小数の計算が正しく動作することを確認
test('should handle decimal numbers correctly', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
expect(add(1.5, 2.5)).toBe(4);
});
// 境界値テスト: 非常に大きな数値の計算が正しく動作することを確認
test('should handle large numbers correctly', () => {
expect(add(Number.MAX_SAFE_INTEGER - 1, 1)).toBe(
Number.MAX_SAFE_INTEGER
);
});
// 境界値テスト: 非常に小さな数値の計算が正しく動作することを確認
test('should handle very small numbers correctly', () => {
expect(add(Number.MIN_SAFE_INTEGER + 1, -1)).toBe(
Number.MIN_SAFE_INTEGER
);
});
// 例外系テスト: Infinityの処理が適切に行われることを確認
test('should handle Infinity correctly', () => {
expect(add(Infinity, 5)).toBe(Infinity);
expect(add(-Infinity, 5)).toBe(-Infinity);
expect(add(Infinity, -Infinity)).toBeNaN();
});
// 例外系テスト: NaNの処理が適切に行われることを確認
test('should handle NaN correctly', () => {
expect(add(NaN, 5)).toBeNaN();
expect(add(5, NaN)).toBeNaN();
expect(add(NaN, NaN)).toBeNaN();
});
// 境界値テスト: 数値の精度限界での動作を確認
test('should handle floating point precision limits', () => {
// 浮動小数点の精度限界での計算結果を確認
expect(add(1e-16, 1e-16)).toBeCloseTo(2e-16);
});
});
このように、仕様を細かく指示すると網羅性が上がる事がわかりました。
また、テストケースごとに「なぜそのケースが必要か」のコメントを入れる事によって、テストの意図が明確になるメリットがあります
まとめ
今回は、実際にいくつかのプロンプトを使ってAIにテストコードを書かせてみました。
試してみて感じたのは、プロンプト次第でテストコードの質が大きく変わるということです。
本記事で紹介したコツとしては、特に次の2つが重要でした。
- 正常系・異常系・境界値など、必要なテスト範囲を明確に示す
- 使用するテストフレームワーク(例:Jest v29など)のバージョンを指定する
これらをプロンプトに書き込むだけで、AIが生成するテストは安定しました。
また、関数の仕様などを丁寧に伝えることで、ほぼそのまま動くテストコードをAIに書かせることも可能だと分かりました。
プロンプトを作る手間はありますが、一度コツを掴めば
誰でも再現性のあるテストコード生成ができる強力な武器になります。
ぜひこの記事の内容を参考に、日々の開発でAIを活用してみてください!
Discussion