GitHub Copilotでユニットテストを楽に構築・運用する

 十分かつ有効なユニットテストが配備されるとさまざまな恩恵が提供されます。リグレッションテストとして使用可能です。またリファクタリングや追加変更もより安全に進められるようになります。
 ただユニットテストは数が多くプロダクトコードと結合しているため、その構築・保守には手間がかかりがちです。特にプロダクトコードのテスト容易性が低いとその手間が大きくなり、ユニットテストの費用対効果が悪化します。
 こうしたユニットテストを取り巻く煩雑な手間についてですが、生成AIのサポートでかなり手間を削減できます。今回は生成AIサービスのGitHub Copilotを例に、ユニットテストの構築・運用を楽にする簡単なテクニックをまとめます。

ユニットテストを生成する

 GitHub Copilotを使うとテストコードのスケルトンやたたき台を容易に生成できます。例えば次のようなコードがあるとします。

target.cpp

#include <utility>
#include <vector>
#include "target.h"

int partition(std::vector<int>& arr, const int low, const int high) {
    const int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[high]);
    return i + 1;
}

void quickSort(std::vector<int>& arr, const int low, const int high) {
    if (low < high) {
        const int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

target.h

#pragma once

#include <vector>

void quickSort(std::vector<int>& arr, const int low, const int high); 

 こうしたファイルに対しては、GitHub Copilotのチャットビューで「test.cppに対するユニットテストを作成してください。別ファイルで作ってください」と指示を打ち込みます(今回は簡単な例なのでこれで済みますが、複雑なプロジェクトに対しては追加のプロンプトが求められます)。
 するとGitHub CopilotはGoogle TestやCMakeなど必要なツールのインストールを許可を取りながら進めます。そして以下のユニットテストファイルを生成します。

test_target.h

#include <gtest/gtest.h>
#include <vector>
#include "target.h"

TEST(QuickSortTest, SortsAlreadySortedVector) {
    std::vector<int> arr = {1, 2, 3, 4, 5};
    quickSort(arr, 0, static_cast<int>(arr.size()) - 1);
    EXPECT_EQ(arr, std::vector<int>({1, 2, 3, 4, 5}));
}

TEST(QuickSortTest, SortsReverseSortedVector) {
    std::vector<int> arr = {5, 4, 3, 2, 1};
    quickSort(arr, 0, static_cast<int>(arr.size()) - 1);
    EXPECT_EQ(arr, std::vector<int>({1, 2, 3, 4, 5}));
}

TEST(QuickSortTest, SortsRandomOrderVector) {
    std::vector<int> arr = {3, 1, 4, 1, 5};
    quickSort(arr, 0, static_cast<int>(arr.size()) - 1);
    EXPECT_EQ(arr, std::vector<int>({1, 1, 3, 4, 5}));
}

TEST(QuickSortTest, SortsSingleElementVector) {
    std::vector<int> arr = {42};
    quickSort(arr, 0, static_cast<int>(arr.size()) - 1);
    EXPECT_EQ(arr, std::vector<int>({42}));
}

TEST(QuickSortTest, EmptyVectorDoesNothing) {
    std::vector<int> arr;
    EXPECT_NO_THROW(quickSort(arr, 0, static_cast<int>(arr.size()) - 1));
    EXPECT_TRUE(arr.empty());
}

 またGitHub Copilotは同時にCMakeLists.txtといったビルド・実行のための設定ファイルも自動生成・更新し、生成したユニットテストをビルド・実行可能にします。
 実行結果です:

[       OK ] QuickSortTest.SortsRandomOrderVector (0 ms)
[ RUN      ] QuickSortTest.SortsSingleElementVector
[       OK ] QuickSortTest.SortsSingleElementVector (0 ms)
[ RUN      ] QuickSortTest.EmptyVectorDoesNothing
[       OK ] QuickSortTest.EmptyVectorDoesNothing (0 ms)
[----------] 5 tests from QuickSortTest (0 ms total)

[----------] Global test environment tear-down
[==========] 5 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 5 tests.

 注意点として、生成されたユニットテストのテストケースは通常そのまま信用できるのではなく、修正や補強が必要です。ただたたき台を自動生成させれば、ゼロからテストコードを手打ちするよりはるかに楽にユニットテストを確保できます。

テストコードの実装を補完させる

 GitHub Copilotでは、テストコードのプログラミングの中で細部のスニペット生成や実装候補の提案も高度に実施してくれます。
 例えば前項で生成したユニットテストのファイルで「//巨大な要素数で動作することを確認 」とコメントを入力してコード提案を実行させると、コメントに沿ったテストメソッドが提案されます。

 また高度なコード補完・コードスニペット生成手段としても有効です。テストメソッド中に「E」を入力すると、EXPECT_EQのアサーションメソッドの記述が提案されます。


プロダクトコードの変更に追従させる

 GithubCopilotではプロダクトコードの変更対応も高度に自動化できます。
 例えばプロダクトコードに次の関数を追加したとします。

target.cpp

void BubbleSort(std::vector<int>& arr) {
    const int n = static_cast<int>(arr.size());
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
            }
        }
    }
}

 こうしたプロダクトコード変更に対しては、チャットビューで「target.cppを網羅するユニットテストを作成してください。既存のユニットテストtest_target.cppを修正して対応してください」といったプロンプトを入力すると、プロダクトコードの変更に追従したテストコード修正が実施されます。今回の例では、追加関数に対する追加テストコードが作成されます。

 また例えば追加した「BubbleSort」を「bubbleSort」に置換した場合に対するセルフヒーリングも可能です。例えば「target.cppの変更に対応して、ユニットテストを修正してください」とプロンプトを実行すると、置換に対応したヘッダファイルとテストコード修正が実施され、実行可能確認が行われます。

テスト駆動開発のサポートを行う

 テスト駆動開発は、生成AIとかなり親和性が高いプログラミングスタイルだと前々からよく言われています。それは事実なのですが、注意点として生成AIを前提とした場合、テスト駆動開発の進め方やサイクルサイズが変化します。
 通常テスト駆動開発は次のステップで進めます。

1. 仕様をテストリストとして明文化する
2. テストリストの1項目について、テストを書いて失敗させる
3. テストの失敗を成功に変えるようにプロダクトコードを変更する

※このうち2、3をテストリスト全てに対応するまで繰り返す
※また一定のプロダクトコードがたまったら、適宜リファクタリングを実施してプロダクトコードを綺麗にする

 これを生成AI活用を前提とすると、次のような流れになります。

1. 仕様をテストリストとして明文化する
2. テストリストについて、プロダクトコードとテストコードを一緒に生成する
3. テストコードのテスト設計・テスト実装を適切なものに修正する
4. テスト成功状態を維持しながら、プロダクトコードを適切なものに修正する

 通常のテスト駆動開発では、テスト失敗→テスト成功のテストファーストのサイクルは数十秒から数分の短期サイクルです。しかし生成AIを使った場合、一気にすべてを生成できるため、サイクルが大きくなります。さらにテストを失敗するステップの恩恵が小さくなり、多くの場合スキップすることになります。