AIを使ったテスト開発

Description

GitHub Copilotを使ってコード生成を行う際、文脈をAIに提供していないのにテストケースの網羅性を期待することは難しいです。 GitHub Copilot だけでなく、ChatGPT などのツールも駆使して、テストケースを書きましょう。

Context

GitHub Copilotは、プログラマが手動でコードを書くことを減らすことを目的とした、AIによるコード自動生成ツールです。 GitHub Copilotを使用すると、AI はあなたが慣れ親しんでいない構文やアプローチで素晴らしいコードを生成するかもしれません。 しかし、あなたにとって可読性が低いコードは、メンテナンス性を低下させる可能性があります。 そのため、GitHub Copilotを使用しながらも、しっかりとしたテストコードを用意することが重要です。 テストコードは、プログラムの品質を保証する上で重要な役割を果たすため、自動生成されたコードのテストケースの網羅性は必要不可欠です。

Problem

GitHub Copilotを使用してテストコードを書く場合、詳細な文脈を与えない限り、十分な網羅性のあるテストコードを自動生成することはできません。

例として GitHub Copilotを使用して、以下のテストコードを書くとします。このコードは、2つの数値の掛け算を行います。

def multiply(a, b):
    return a * b

このコードに対して、GitHub Copilotを使用すると、簡単にテストコードを生成することができます。

import unittest

# multiply()のテストコードを書いてください
class TestMultiply(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(5, 10), 50)
        self.assertEqual(multiply(7, 7), 49)
        self.assertEqual(multiply(5, 5), 25)
        self.assertEqual(multiply(2, 2), 4)
        self.assertEqual(multiply(3, 3), 9)

unittest.main()

さて、GitHub Copilot が TDD にも使えることがわかりました。しかし、こちらで紹介したテストケースは本当に良いコードでしたでしょうか。 「とんでもない!!」GitHub Copilot が提供したテストコードは素晴らしいテストケースには遥か及びません。

ここでいくつかの問題点を指摘します。 まず、テストの重複があります。いくつかのテストケースが、同じ結果を期待しています。 これはテストの重複を意味し、無駄なテストを実行していることになります。

二つめの問題点はエラーを検出できていないことです。 このテストケースでは、multiply()関数が期待通りに動作していることを確認していますが、エラーが発生した場合には検出できません。 例えば、string 型が渡された場合にエラーを返す処理はありません。

今回テストに通った multiply()関数を使って以下の処理を渡すと、想定した動作になっていないことがわかります。

print(multiply(2, "Hello World!"))
# Hello World!Hello World!

例示した中にはテストの重複があったり、エラーを検出できていなかったりしました。 もしコードを書くのであれば、以下のように書いてみるのが良いかもしれません。

以下の場合、1行1行、条件を指定して確認しながら GitHub Copilot とコミュニケーションをとっています。

# multiply()のテストコードを以下の条件に従って書いてください。
# Condition: 成功ケースを含める
class TestMultiply(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        # Condition: 失敗ケースを含める
        self.assertIsNot(multiply(2, 3), 7)
        # Condition: リターンが数値型であることを確認する
        self.assertIsInstance(multiply(2, 3), int)

だいぶ、前よりは改善しましたね。 一方で、これでも網羅性にかけるのです。 ではどのようにしたら、GitHub Copilot をより活用して網羅性のあるテストを書くことができるのでしょうか。

Solution

GitHub Copilotを使用してテストコードを書く場合は、詳細な文脈を与えることが必要です。ChatGPTのような自然言語処理モデルを使用して、より網羅的なテストケースを作成することができます。

以下のようなプロンプトを ChatGPT に渡してみましょう。

以下のコードのテストコードを漏れなく書く際、どのようなテストケース・条件があるでしょうか。箇条書きにしてください

def multiply(a, b):
    return a * b

すると ChatGPT は以下のような結果を返します。素晴らしく網羅性のあるテストケースのように見えます。

以下は、multiply()関数のテストコードに含めるべきテストケースの例です。

  • 2つの正の整数を入力した場合、正しい結果を返すことを確認する。

  • 2つの負の整数を入力した場合、正しい結果を返すことを確認する。

  • 1つの正の整数と1つの負の整数を入力した場合、正しい結果を返すことを確認する。

  • 1つの入力が0である場合、結果が0であることを確認する。

  • 1つの入力が小数である場合、エラーが発生することを確認する。

  • 1つの入力が文字列である場合、エラーが発生することを確認する。

  • 整数以外の入力が与えられた場合、エラーが発生することを確認する。

上記の結果をもとにテストコードを GitHub Copilot と一緒に書いた結果以下のようになりました。だいぶ見栄えが良くなりました。

# multiply()のテストコードを以下の条件に従って書いてください。
# Condition: 成功ケースを含める
class TestMultiply(unittest.TestCase):
    def test_multiply(self):
        # 2つの正の整数を入力した場合、正しい結果を返すことを確認する。
        self.assertEqual(multiply(3, 4), 12)
        # 2つの負の整数を入力した場合、正しい結果を返すことを確認する。
        self.assertEqual(multiply(-3, -4), 12)
        # 1つの正の整数と1つの負の整数を入力した場合、正しい結果を返すことを確認する。
        self.assertEqual(multiply(3, -4), -12)
        # 1つの入力が0である場合、結果が0であることを確認する。
        self.assertEqual(multiply(3, 0), 0)
        # 1つの入力が小数である場合、エラーが発生することを確認する。
        self.assertRaises(ValueError, multiply, 3, 0.1)
        # 1つの入力が文字列である場合、エラーが発生することを確認する。
        self.assertRaises(ValueError, multiply, 3, "a")
        # 整数以外の入力が与えられた場合、エラーが発生することを確認する。
        self.assertRaises(TypeError, multiply, 3, "a")

一方でこれも完璧かというとそうではありません。 1つの入力が小数である場合、エラーが発生することを確認する必要があるのかは実装によりますし、最後の2つのテストケースは同じエラーをテストしています。 まだ修正の余地はありますが、テストコードを書く書き初めの段階でここまで一瞬で辿り着けることは素晴らしいことです。

Resulting Context

GitHub Copilotを使用してコードを自動生成する際には、テストコードの網羅性に注意する必要があります。 詳細な文脈を与え、ChatGPTなどの自然言語処理モデルを使用してより網羅的なテストケースを作成することができます。 しかし一方で完璧なテストコードを自動生成することはできないため、手動で修正する必要があることに留意する必要があります。 テストコードはプログラム品質を保証するために非常に重要であり、網羅的なテストケースを持つことは必要不可欠です。