🧪

gpt_test_gen: GPT-3でDartのテストコードを書いてくれるツール

2023/03/15に公開

これはなに?

https://pub.dev/packages/gpt_test_gen

https://github.com/Kurogoma4D/gpt_test_gen

巷で噂のChatGPT(OpenAI API)を使ったテストコード生成ツールです。
ソースコードのパスを指定すると、その中身の関数などに対してテストコードを生成してくれます。

使い方

pub.devに公開してあるので、下記コマンドで自分のプロジェクトに追加します。

$ dart pub add dev:gpt_test_gen

あるいは直接dev_dependenciesに追加しても大丈夫です。

dev_dependencies:
  gpt_test_gen:

追加できたら、後はコマンドを叩くだけです。

$ dart run gpt_test_gen -t <token> -i <your_source_file_path>

<token> はOpenAIアカウントで発行できるAPIトークンです。自身のアカウントでログインして以下から発行してください。

https://platform.openai.com/account/api-keys

your_source_file_path は、テストコードを書いてほしいDartのソースファイルのパスを指定します。
例えば、lib/entities/foo_state.dart などになります。

やっていること

やっていることは至極単純、

  1. 各引数で必要な情報を受け取る
  2. OpenAI APIにプロンプトとともにファイルの中身を展開して投げる
  3. 結果をテストコードファイルとして出力する

です。

情報を受け取る

DartはCLIツールを作るのに便利なpackageがいくつか用意されていて、args もその一つです。
ArgParser というクラスが提供されるので、それを使うことで、コマンドライン引数をいい感じに解釈する事ができます。

今回は内部で GptTestGenerator というクラスを用意しているので、引数を解釈した後はそっちに処理を全て移譲します。

const _tokenKey = 'token';
const _inputKey = 'input';
const _maxTokensKey = 'max-tokens';

void main(List<String> args) async {
  // parserを用意して、argsを読み込ませて解釈する
  final argParser = ArgParser()
    ..addOption(_tokenKey, abbr: 't')
    ..addOption(_inputKey, abbr: 'i')
    ..addOption(_maxTokensKey);
  final result = argParser.parse(args);

  final token = result[_tokenKey] as String?;
  if (token == null) {
    print('Please specify OpenAI API token like: `-t <token>`');
    exit(1);
  }

  final inputPath = result[_inputKey] as String?;
  if (inputPath == null) {
    print('Please specify original source file path like: `-i lib/foo.dart`');
    exit(1);
  }

  final generator = GptTestGenerator(token: token);
  await generator.generate(
    inputPath: inputPath,
    maxTokens: result[_maxTokensKey] as String?,
  );
}

APIに投げる

必要な情報をもらったら、GptTestGenerator の中で処理を行っていきます。
入力ファイルの中身を読み取ってAPIコールをします。

  Future<void> generate({
    required String inputPath,
    required String? maxTokens,
  }) async {
    final inputFile = File(inputPath);
    final original = await inputFile.readAsString();

    final result = await _callApi(original, maxTokens);

    // 略
  }

固定のプロンプトの中にファイルの内容を埋め込んでAPIコール、結果を抜き出してreturnします。
プロンプトは Please write a unit test of the following Dart code. No explanatory text is required as we would like to save your output as source code as is. Test cases should also include boundary conditions. で、ほぼ参考記事のものと同じです(Swiftの部分はDartに置き換えています)。

以下のSwiftコードのユニットテスト(XCTest)を書いてください。出力したものをそのままソースコードとして保存したいので、説明文は不要です。

https://zenn.dev/zozotech/articles/4f1763c83db76e#xctestの自動生成

アレンジとして、「テストケースには境界条件を含めてください( Test cases should also include boundary conditions. )」という一文を盛り込んでいます。

  Future<String> _callApi(String original, String? maxTokens) async {
    final url = Uri.parse('https://api.openai.com/v1/completions');
    final prompt =
        'Please write a unit test of the following Dart code. No explanatory text is required as we would like to save your output as source code as is. Test cases should also include boundary conditions.\n\n$original';
    print(prompt);
    final body = jsonEncode({
      "model": "text-davinci-003",
      "prompt": prompt,
      "max_tokens": maxTokens ?? 1000,
    });
    final result = await http.post(
      url,
      headers: {
        'Content-Type': ContentType.json.value,
        'Authorization': 'Bearer $token',
      },
      body: body,
    );

    // エラー処理

    // レスポンスから出力を取り出してreturn
  }

最後に出力結果をファイルに出力します。
出力するファイルのパスは、入力として与えられたパスの lib/ の部分を test/ に置き換え、ファイル名の末尾に _test をつけたものになっています。

    final result = await _callApi(original, maxTokens);

    final outputPath = inputPath
        .replaceFirst(r'lib', 'test')
        .replaceFirst(r'.dart', '_test.dart');
    final outputFile = File(outputPath);

    await outputFile.writeAsString(result);

動作イメージ

https://twitter.com/Krgm4D/status/1635630491184566272

今後の展望

今後真面目にメンテナンスするかは不明ですが、するとなったら

  • モデルの指定
  • パス指定の柔軟性確保
    • lib/ をつけなくていいようにする
  • (メンテとは違うけど)VS Code拡張化

あたりですかね?パッと思いつくのは…

Sun* Developers

Discussion