ブログトップLangChain(ChatGPT)を使って単体テスト仕様書を作成する

LangChain(ChatGPT)を使って単体テスト仕様書を作成する

2024年08月23日
投稿者:さえ

はじめに

こんにちは、さえです。

このブログを見ている方って、やっぱりプログラマーの方が多いのでしょうか?

もし、プログラマーの方だとしたら、お聞きしたいのが、皆さん、テストってどうやってますか?

テストファースト、テスト駆動云々と、テストケースのプログラムを書いて、自動テストする方が多いのでしょうか? それとも、昔ながらのテスト仕様書を書いて、自力でテストする方が多いのでしょうか?

まぁ、どっちにしろ、プログラムの品質を高めるため・担保するためにテストが必要なわけですが、納品物の為にテスト仕様書を用意(書く)しないといけないというのも未だにある現実なのかなぁと思っています。

ということで、今回は、LLM(ChatGPT)にテスト仕様書を書いてもらうという試みです。

テスト仕様書と言っても、単体テスト、結合テスト、総合テスト、etc.etc. いろいろとあるわけですが、今回は単体テストの仕様書を作ってみようと思います。

単体テスト仕様書を作る

本来は、プログラム仕様書から作成すべきなんでしょうけど、今回は作ったプログラムから作らせてみようかと思います。(正直、プログラムから作るということ自体、本来はNGかなと思っています。でも、今回はChatGPTを使って、さくっと作れるかという試みの為、許してください。)

プログラムのソースファイルを読み込ませ、それをChatGPTに投げ、解析してもらい、単体テスト仕様書のデータ(TSV形式)を作ってもらいます。

出力されたTSVデータをPandasで読込み、Excelファイル化して、openpyxlでExcelを良い感じに加工します。(罫線とか)

事前に用意するもの

  • OpenAPI KEY ※今回も説明しませんが、過去の記事から取得の仕方を参照してください。
  • Python環境

※今回は先日リリースされたGPT-4o-miniを使ってみようかと思ったのですが、出力精度があまり良くなかったので、GPT-4oにしました。使用の際には料金が発生しますので、十分に注意して実施してください。実施する場合は自己責任でお願いします。(一切の保証はいたしかねます。)

実行環境

  • Python 3.11
  • langchain==0.1.16
  • langchain_openai==0.1.3
  • openai==1.21.2
  • openpyxl==3.1.2
  • pandas==2.2.2

langchain, langchain_openai, openai, openpyxl, pandasのモジュールはpipコマンドで入れてください。

Anacondaやpyenvなどで仮想環境を作っておくと便利です。(仮想環境の作り方は今回割愛しますが、過去の投稿等見てください。)

プログラムの流れ

以下の流れで実装してきます。

  1. ソースファイルのパスを入力する。
  2. ファイルの拡張子から、プログラムの種類を判別する。
  3. 単体テスト仕様書を作成するLLM(ChatGPT)を実行する。(結果はTSV形式で出力する。)
  4. 結果(TSVデータ)をテンポラリファイルに保存する。
  5. pandasでExcelファイルに変換する。
  6. openpyxlで幅・罫線などセルの設定を行い、上書き保存する。

実行ソースは以下になります。

【GenerateTestcaseSpecification.py】

                            #!/usr/bin/env python
                            # coding: utf-8
                            
                            import os
                            from datetime import datetime
                            import pandas as pd
                            import openpyxl
                            from openpyxl.styles.borders import Border, Side
                            
                            from langchain_openai import ChatOpenAI
                            from langchain_core.prompts import PromptTemplate
                            from langchain_core.output_parsers import StrOutputParser
                            
                            
                            os.environ["OPENAI_API_KEY"] = "sk-XXXXXXXXX" # ここに取得したAPI KEYを設定する
                            
                            
                            # テスト仕様書文章生成
                            def generate_testcase_specification_ai(program_language, program):
                                # プロンプトの定義
                                template = """
                            あなたは優秀なプログラマーです。
                            ステップバイステップで以下の処理を行ってください。
                            まず、以下の{program_language}のプログラムから、処理内容を把握してください。
                            続いて、以下の条件に従って単体テスト仕様書を作ってください。
                            単体テスト仕様書はTSV形式で出力してください。
                            出力はTSVデータのみとし、説明やタイトルは不要。
                            
                            # 条件:
                            ・関数・クラス・メソッド単位でロジックごとに処理内容をチェックするホワイトボックステストとインターフェースをチェックするブラックボックステストのテストケースを作ってください。
                            ・列項目は、'テストケースID'、'関数・クラス・メソッド名'、'テストケースの概要'、'テスト手順'、'期待結果'、'結果'、'備考'としてください。
                            ・'結果'、'備考'のカラムは空欄にしてください。
                            ・'テストケースID'は1から連番で振ってください。
                            ・各カラムは、シングルクォーテーションまたはダブルクォーテーションで囲まないでください。
                            ・1テストケースで1行としてください。
                            ・タイトル行は不要です。
                            
                            # プログラム:'''
                            {program}
                            '''
                            
                            # 出力:"""
                            
                                prompt = PromptTemplate(
                                    input_variables=['program_language', 'program'],
                                    template=template,
                                )
                            
                                # LLMの生成
                                llm = ChatOpenAI(model_name="gpt-4o", temperature=0)
                            
                                # チェーンの生成
                                chain = prompt | llm | StrOutputParser()
                            
                                # 推論の実行
                                output = chain.invoke({"program_language": program_language, "program": program})
                            
                                return output
                            
                            
                            # テスト仕様書ファイル生成
                            def generate_testcase_specification_file(test_specification_doc, excel_file):
                                # 結果をテンポラリTSVに出力
                                date_s = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3]
                                temp_tsv = f'./temp_{date_s}.tsv'
                                with open(temp_tsv, 'w', encoding='UTF-8') as f:
                                    f.write(test_specification_doc)
                                
                                # TSVからPandas経由でExcelに出力
                                col_names = ['テストケースID', '関数・クラス・メソッド名', 'テストケースの概要', 'テスト手順', '期待結果', '結果', '備考']
                                df = pd.read_csv(temp_tsv, names=col_names, sep='\t')
                                df.to_excel(excel_file, index=False)
                                
                                # テンポラリTSVを削除
                                os.remove(temp_tsv)
                                
                                # Excelの幅を設定する
                                book = openpyxl.load_workbook(excel_file)
                                sheet = book['Sheet1']
                                sheet.column_dimensions['A'].width = 15
                                sheet.column_dimensions['B'].width = 30
                                sheet.column_dimensions['C'].width = 45
                                sheet.column_dimensions['D'].width = 45
                                sheet.column_dimensions['E'].width = 45
                                sheet.column_dimensions['F'].width = 10
                                sheet.column_dimensions['G'].width = 40
                                
                                border = Border(top=Side(style='thin', color='000000'), 
                                            bottom=Side(style='thin', color='000000'), 
                                            left=Side(style='thin', color='000000'),
                                            right=Side(style='thin', color='000000'))
                                
                                # 全セルを上揃え、折り返しに設定する&枠線を設定する
                                for row in sheet.iter_rows():
                                    for cel in row:
                                        cel.alignment = openpyxl.styles.Alignment(vertical="top", wrap_text=True)
                                        cel.border = border
                            
                                # Excelファイルを上書き保存する
                                book.save(excel_file)
                            
                            
                            def main():
                                # ファイルパスを入力する
                                input_file_path = input('プログラムのファイルパスを入力してください。: ')
                                input_file_path = input_file_path.strip('"')
                                if os.path.isfile(input_file_path) == False:
                                    print('指定されたファイルが存在しません。')
                                    return
                            
                                # ファイルの拡張子を判定する
                                ALLOWED_EXTENSIONS = [
                                    ['C#', '.cs'],
                                    ['Java', '.java'],
                                    ['Kotlin', '.kt'],
                                    ['C++', '.cpp'],
                                    ['C', '.c'],
                                    ['Python', '.py'],
                                    ['Ruby', '.rb'],
                                ]
                            
                                matching_language = None
                                base_path = os.path.basename(input_file_path)
                                base_name, ext = os.path.splitext(base_path)
                                for item in ALLOWED_EXTENSIONS:
                                    if item[1] == ext:
                                        matching_language = item[0]
                                        break
                            
                                if matching_language == None:
                                    print('対応していないファイルです。')
                                    return
                            
                                try:
                                    # ソースファイル読込み
                                    print('ソースファイル読込み')
                                    with open(input_file_path, 'r', encoding='UTF-8') as f:
                                        program = f.read()
                            
                                    # 単体テスト仕様書文章生成
                                    print('単体テスト仕様書文章生成')
                                    test_specification_doc = generate_testcase_specification_ai(matching_language, program)
                                    print(test_specification_doc)
                            
                                    # 単体テスト仕様書ファイル生成
                                    print('単体テスト仕様書ファイル生成')
                                    output_file = f'./{base_name}.xlsx'
                                    generate_testcase_specification_file(test_specification_doc, output_file)
                            
                                    print('単体テスト仕様書ファイル生成完了:', output_file)
                                except Exception as e:
                                    print(e)
                                
                            
                            if __name__ == '__main__':
                                main()
                        

実行する

仮想環境のコンソールから、以下のコマンドでプログラムを実行してみてください。

                            >python GenerateTestcaseSpecification.py
                        

プログラムが起動すると、「プログラムのファイルパスを入力してください。:」と表示されるので、ソースファイルのパス(フルパスまたは相対パス)を入力してください。

入力後、LLM(ChatGPT)が呼ばれ、単体テスト仕様書のデータが作成され、ファイル名と同じExcelファイルが出力されます。

試しに、このプログラム自体(GenerateTestcaseSpecification.py)の単体テスト仕様書を作らせてみました。

以下、出力されたExcelファイルのキャプチャです。

出力された単体テスト仕様書
【図:出力された単体テスト仕様書】

どうでしょうか。

あまりいろんなパターンで試してないので、質については何とも言えないのですが、今回の場合は、それっぽく書かれていると思います。

おわりに

今回はChatGPT(LLM)とLangChainを使って、プログラムファイルから単体テスト仕様書を作らせてみました。

始めの方にも書きましたが、プログラムからテスト仕様書を起こすこと自体、あまり良いことではないのですが、それでも、ドキュメントが無い過去の遺産となったプログラムのメンテナンスなどで役立つこともあるかと思います。

また、今回は単体テスト仕様書でしたが、プログラム設計書を書かせるプロンプトに変えることもできるのではないかと思います。(試してはないですが。。。) その場合、同様に過去の遺産プログラムの整理に役立つのかなと。

ここまで長々お付き合いいただきまして、ありがとうございました。

    カテゴリ別一覧