top of page

Github Copilot にテストコードを書かせてみた

最近、Github Copilot の機能について調査をしておりまして、今回はテストコードの作成についてご紹介したいと思います。


テスト対象となるコードは以下になります。

内容はよくある計算処理をまとめたものになります。

ちなみに、こちらのコードはほぼ全て Github Copilot の補完機能で書きました。

class Calculation():
    """計算処理を行うクラス
    """

    @classmethod
    def calc_sum(cls, numList):
        """
        引数のリストの合計値を求める
        """
        # nummList が空の場合
        if not numList:
            return 0

        # 引数が数字のみである事を確認
        for num in numList:
            if not isinstance(num, (int, float)):
                return "引数には数値のみを入力してください"

        # 合計値の演算結果を返す
        return sum(numList)

    @classmethod
    def calc_average(cls, numList):
        """
        引数のリストの平均値を求める
        """
        # nummList が空の場合
        if not numList:
            return 0

        # 引数が数字のみである事を確認
        for num in numList:
            if not isinstance(num, (int, float)):
                return "引数には数値のみを入力してください"

        # 平均値の演算結果を返す
        return sum(numList) / len(numList)

    @classmethod
    def cosine_similarity(cls, x, y):
        """
        コサイン類似度を求める
        x: ベクトル
        y: ベクトル
        """
        # x, y が空の場合
        if not x or not y:
            return "ベクトルが空です"
        
        # x, y に数値以外が含まれていないかを確認
        for xi, yi in zip(x, y):
            if not isinstance(xi, (int, float)) or not isinstance(yi, (int, float)):
                return "ベクトルには数値のみを入力してください"

        # x, y の次元数が異なる場合
        if len(x) != len(y):
            return "ベクトルの長さが等しくありません"

        # x, y のノルムを求める
        x_norm = sum([xi**2 for xi in x])  0.5
        y_norm = sum([yi2 for yi in y]) ** 0.5

        # 内積を求める
        dot_product = sum([xi  yi for xi, yi in zip(x, y)])

        # コサイン類似度の演算結果を返す
        return dot_product / (x_norm  y_norm)

    @classmethod
    def calc_vector(cls, x, y, operation):
        """
        ベクトルの演算を行う
        x: ベクトル
        y: ベクトル
        operation: 計算方法(和, 差, 内積)
        """
        # x, y が空の場合
        if not x or not y:
            return "ベクトルが空です"
        
        # x, y に数値以外が含まれていないかを確認
        for xi, yi in zip(x, y):
            if not isinstance(xi, (int, float)) or not isinstance(yi, (int, float)):
                return "ベクトルには数値のみを入力してください"

        # x, y の次元数が異なる場合
        if len(x) != len(y):
            return "ベクトルの長さが等しくありません"

        # 計算方法が不正な場合
        if operation not in ["和", "差", "内積"]:
            return "計算方法が不正です"

        # ベクトルの演算
        if operation == "和":
            return [xi + yi for xi, yi in zip(x, y)]
        elif operation == "差":
            return [xi - yi for xi, yi in zip(x, y)]
        elif operation == "内積":
            return sum([xi * yi for xi, yi in zip(x, y)])

ここから、テストコードを作成をしていきますが

今回は2パターンの作成方法をご紹介します。


①自動作成

以下の操作でテストコードを自動で作成してくれます。

Calculation() にカーソルを当てる -> 右クリック -> Copilot -> Generate Tests

作成されたコードは以下になります。

※import 等は割愛しています。

class TestCalculation(unittest.TestCase):

    def test_calc_sum(self):
        # Test case 1: Empty list
        self.assertEqual(Calculation.calc_sum([]), 0)

        # Test case 2: List with integers
        self.assertEqual(Calculation.calc_sum([1, 2, 3, 4, 5]), 15)

        # Test case 3: List with floats
        self.assertEqual(Calculation.calc_sum([1.5, 2.5, 3.5, 4.5, 5.5]), 17.5)

        # Test case 4: List with mixed integers and floats
        self.assertEqual(Calculation.calc_sum([1, 2.5, 3, 4.5, 5]), 16)

        # Test case 5: List with non-numeric values
        self.assertEqual(Calculation.calc_sum([1, 2, '3', 4, 5]), "引数には数値のみを入力してください")

    def test_calc_average(self):
        # Test case 1: Empty list
        self.assertEqual(Calculation.calc_average([]), 0)

        # Test case 2: List with integers
        self.assertEqual(Calculation.calc_average([1, 2, 3, 4, 5]), 3)

        # Test case 3: List with floats
        self.assertEqual(Calculation.calc_average([1.5, 2.5, 3.5, 4.5, 5.5]), 3.5)

        # Test case 4: List with mixed integers and floats
        self.assertEqual(Calculation.calc_average([1, 2.5, 3, 4.5, 5]), 3)

        # Test case 5: List with non-numeric values
        self.assertEqual(Calculation.calc_average([1, 2, '3', 4, 5]), "引数には数値のみを入力してください")

    def test_cosine_similarity(self):
        # Test case 1: Empty vectors
        self.assertEqual(Calculation.cosine_similarity([], []), "ベクトルが空です")

        # Test case 2: Vectors with integers
        self.assertAlmostEqual(Calculation.cosine_similarity([1, 2, 3], [4, 5, 6]), 0.9746318461970762)

        # Test case 3: Vectors with floats
        self.assertAlmostEqual(Calculation.cosine_similarity([1.5, 2.5, 3.5], [4.5, 5.5, 6.5]), 0.9746318461970762)

        # Test case 4: Vectors with mixed integers and floats
        self.assertAlmostEqual(Calculation.cosine_similarity([1, 2.5, 3], [4.5, 5, 6]), 0.9746318461970762)

        # Test case 5: Vectors with non-numeric values
        self.assertEqual(Calculation.cosine_similarity([1, 2, '3'], [4, 5, 6]), "ベクトルには数値のみを入力してください")

        # Test case 6: Vectors with different dimensions
        self.assertEqual(Calculation.cosine_similarity([1, 2, 3], [4, 5]), "ベクトルの長さが等しくありません")

    def test_calc_vector(self):
        # Test case 1: Empty vectors
        self.assertEqual(Calculation.calc_vector([], [], "和"), "ベクトルが空です")

        # Test case 2: Vectors with integers (addition)
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5, 6], "和"), [5, 7, 9])

        # Test case 3: Vectors with floats (subtraction)
        self.assertEqual(Calculation.calc_vector([1.5, 2.5, 3.5], [4.5, 5.5, 6.5], "差"), [-3, -3, -3])

        # Test case 4: Vectors with mixed integers and floats (dot product)
        self.assertEqual(Calculation.calc_vector([1, 2.5, 3], [4.5, 5, 6], "内積"), 29.5)

        # Test case 5: Vectors with non-numeric values
        self.assertEqual(Calculation.calc_vector([1, 2, '3'], [4, 5, 6], "和"), "ベクトルには数値のみを入力してください")

        # Test case 6: Vectors with different dimensions
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5], "和"), "ベクトルの長さが等しくありません")

        # Test case 7: Invalid operation
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5, 6], "invalid"), "計算方法が不正です")

if __name__ == '__main__':
    unittest.main()

ご覧の通り、各関数の各条件分岐まで網羅的に作成してくれています。

時間も数秒程しかかからず作成してくれましたので、非常に便利ですね^^


しかし、以下2点の問題があります。


・失敗ケースが存在する場合がある

Github Copilot が全て作成しているにも関わらず、想定結果と実際の結果に相違があるケースがいくつかありました。


・仕様を考慮できていない場合がある

テストケースの想定結果でそもそも関数の仕様(返り値の型など)を考慮できていないケースも同様にいくつかありました。



②コメントベースで設計

コメントでテストケースを設計し、その内容に応じたテストケースを作成する事も可能です。

まずは、作成された以下のコードをご覧下さい。

class CalculationPositiveTests(unittest.TestCase):
    # Calculation の正常系テスト
    """
    テストケース
    calc_sum
        - numList が数字のみ

    calc_average
        - numList が数字のみ
    
    cosine_similarity
        - x, y のベクトルの長さが等しい

    calc_vector
        - x, y のベクトルの長さが等しい
            - operation: 和
            - operation: 差
            - operation: 内積
    """

    def test_calc_sum(self):
        # Test case 1: List with integers
        self.assertEqual(Calculation.calc_sum([1, 2, 3, 4, 5]), 15)

        # Test case 2: List with floats
        self.assertEqual(Calculation.calc_sum([1.5, 2.5, 3.5, 4.5, 5.5]), 17.5)

        # Test case 3: List with mixed integers and floats
        self.assertEqual(Calculation.calc_sum([1, 2.5, 3, 4.5, 5]), 16)

    def test_calc_average(self):
        # Test case 1: List with integers
        self.assertEqual(Calculation.calc_average([1, 2, 3, 4, 5]), 3)

        # Test case 2: List with floats
        self.assertEqual(Calculation.calc_average([1.5, 2.5, 3.5, 4.5, 5.5]), 3.5)

        # Test case 3: List with mixed integers and floats
        self.assertEqual(Calculation.calc_average([1, 2.5, 3, 4.5, 5]), 3)

    def test_cosine_similarity(self):
        # Test case 1: Vectors with integers
        self.assertAlmostEqual(Calculation.cosine_similarity([1, 2, 3], [4, 5, 6]), 0.9746318461970762)

        # Test case 2: Vectors with floats
        self.assertAlmostEqual(Calculation.cosine_similarity([1.5, 2.5, 3.5], [4.5, 5.5, 6.5]), 0.9746318461970762)

        # Test case 3: Vectors with mixed integers and floats
        self.assertAlmostEqual(Calculation.cosine_similarity([1, 2.5, 3], [4.5, 5, 6]), 0.9746318461970762)

    def test_calc_vector(self):
        # Test case 1: Vectors with equal dimensions
        # Test case 1-1: operation: 和
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5, 6], "和"), [5, 7, 9])
        # Test case 1-2: operation: 差
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5, 6], "差"), [-3, -3, -3])
        # Test case 1-3: operation: 内積
        self.assertEqual(Calculation.calc_vector([1, 2, 3], [4, 5, 6], "内積"), 32)


class CalculationNegativeTests(unittest.TestCase):
    # Calculation の異常系テスト
    """
    テストケース
    calc_sum
        - numList が空
        - numList に数値以外が含まれる

    calc_average
        - numList が空
        - numList に数値以外が含まれる
    
    cosine_similarity
        - x, y のベクトルの長さが異なる
        - x, y の何れかが空
        - x, y に数値以外が含まれる

    calc_vector
        - x, y のベクトルの長さが異なる
        - x, y の何れかが空
        - x, y に数値以外が含まれる
        - operation が不正
    """

    def test_calc_sum(self):
        # Test case 1: Empty list
        with self.assertRaises(ValueError):
            Calculation.calc_sum([])

        # Test case 2: List with non-numeric elements
        with self.assertRaises(ValueError):
            Calculation.calc_sum([1, 2, 3, 4, "a"])

    def test_calc_average(self):
        # Test case 1: Empty list
        with self.assertRaises(ValueError):
            Calculation.calc_average([])

        # Test case 2: List with non-numeric elements
        with self.assertRaises(ValueError):
            Calculation.calc_average([1, 2, 3, 4, "a"])

    def test_cosine_similarity(self):
        # Test case 1: Vectors with different dimensions
        with self.assertRaises(ValueError):
            Calculation.cosine_similarity([1, 2, 3], [4, 5])

        # Test case 2: Empty vector
        with self.assertRaises(ValueError):
            Calculation.cosine_similarity([], [4, 5, 6])

        # Test case 3: Vector with non-numeric elements
        with self.assertRaises(ValueError):
            Calculation.cosine_similarity([1, 2, "a"], [4, 5, 6])

    def test_calc_vector(self):
        # Test case 1: Vectors with different dimensions
        with self.assertRaises(ValueError):
            Calculation.calc_vector([1, 2, 3], [4, 5], "和")

        # Test case 2: Empty vector
        with self.assertRaises(ValueError):
            Calculation.calc_vector([], [4, 5, 6], "和")

        # Test case 3: Vector with non-numeric elements
        with self.assertRaises(ValueError):
            Calculation.calc_vector([1, 2, "a"], [4, 5, 6], "和")

        # Test case 4: Invalid operation
        with self.assertRaises(ValueError):
            Calculation.calc_vector([1, 2, 3], [4, 5, 6], "invalid")

例えば、CalculationNegativeTests のテストを作成する際は、以下をプログラマーが書くだけで、あとは Github Copilot が作成してくれる、というものです。

class CalculationPositiveTests(unittest.TestCase):
    # Calculation の正常系テスト
    """
    テストケース
    calc_sum
        - numList が数字のみ

    calc_average
        - numList が数字のみ
    
    cosine_similarity
        - x, y のベクトルの長さが等しい

    calc_vector
        - x, y のベクトルの長さが等しい
            - operation: 和
            - operation: 差
            - operation: 内積
    """

このように簡単にテストケースを設計するだけで、作成してくれるのはやはり便利ですね^^


しかし、こちらもやはり「①自動作成」と同様の問題が確認できました。

従って、どちらのケースも完全に Github Copilot に書かせるのは難しそうです。。。



以上、Github Copilot にテストコードを作成させる方法についてご紹介致しました。


細かく設計する必要があるのであれば、「②コメントベースで設計」を利用しても良いですし、とりあえず最低限網羅したケースがあれば良いのであれば「①自動作成」を使うやり方も有効かも知れません。


もしくは両方を組み合わせて使うのも良いかも知れません。


ぜひ、ご参考に頂ければ幸いです^^

bottom of page