Bedrockを使って作成したコードのテストケースとテストコードを生成してみる

スポンサーリンク
ハンズオン
スポンサーリンク

はじめに

システム開発において、テスト工程はバグを検知するための最後の砦です。しかし、テストケースの洗い出しに漏れが発生したり、テストコードの作成に時間がかかってしまうことはないでしょうか?

実際、テストケースの洗い出しには経験や勘が必要で、テスト観点が不足すると重要なケースを見落としてしまうことがあります。また、テストコードはリグレッションテストの観点で長期的には有用ですが、短期的にはコストパフォーマンスが悪く、手間がかかると感じることもあるでしょう。

そこで本記事では、作成したTypeScriptコードをインプットとして、Bedrock(生成AI)を活用してテストケースやJestのテストコードを自動生成する方法を紹介します。

具体的には、S3にアップロードしたコードをトリガーにLambdaを実行し、Bedrockでテストケースとテストコードを生成し、S3へアップロードするという一連の流れを解説します。

結論から言うと、本記事で動作確認をした結果では、出力するテストケースとテストコードは完璧ではなかったですが、0から考えるよりは効率的にテストケースやテストコードを作成できます。

記事内では、実際の画面を使って手順をわかりやすく説明しているため、AWSに慣れていない方でも安心して取り組めます。ぜひ参考にしてみてください!

環境構成

環境構成は以下です。

事前準備

Bedrockのモデルの有効化

Bedrockの基盤モデル(Anthoropic社のClaude 3.5 Sonet)を有効にする(以下の記事が参考になります)
Amazon Bedrockの基盤モデルを有効化して使ってみる
※基盤モデルの有効化はリージョンごとで設定が必要です。API GatewayやLambdaと同じリージョンでモデルを有効化してください。

システムプロンプトの作成

テストケースやテストコードの作成をするためのシステムプロンプト(指示文)を作成します。
今回は以下の内容をシステムプロンプトに設定します。テスト条件は指定の条件があればそちらを利用してください。

# 指示文
– 以下の条件に基づいて入力されたTypeScriptのコードのテストケースとテストコードを生成してください。

# 条件

– 入力されたコードが不正な場合は、resultはfalseとしてください。正常な場合はtrueとしてください。
– テストケースは以下の項目を出力してください。()内は説明のため出力時には不要です。
  – テスト項目件名(テストの件名)
  – テスト条件(インプットのパラメタやテスト環境の条件設定)
  – テスト結果(実行した結果の予想される復帰値やメッセージ json形式ではなく文章でわかるようにしてください。)
– 分岐網羅 (C1カバレッジ)でテストケースを生成してください。
– テストコードはJestを使ってください
– 処理内で関数を使う場合、mockを使ってください。
– テストケースで洗い出しをした項目をテストコードとしてください。
– 結果はJSONの出力形式で復帰してください。その他のメッセージは不要です。
– エスケープが必要な文字列はエスケープしてください。
– NodeJsのSON.parseを使って文字列をオブジェクト化するのでparseできるように文字列をエスケープしてください。
– jsonのキーと値は”key”: “value”といったように「”」でくくってください。
– json値に「”」は使わず、「’」を使ってください。例)”key”: “value: ‘message text'”
– テストコード内の改行コード(\n)はjson形式で識別できるように「\\n」で出力してください。

# 出力形式

{
  “result”: 【true or false】
  “items”: [
    {
      “title”: “【テスト項目件名】”,
      “condition”: “【テスト条件】”,
      “prediction”: “【テスト結果】”,
    },
    ・
    ・
    ・
  ],
  “code”: “【テストコード】”
}

設定手順

以下の流れで設定していきます。

  1. S3バケットの作成
  2. Lambda関数の作成
  3. Lambda関数のロール設定

1. S3バケットの作成

以下の2つのバケットを作成します。

  1. 作成したTypeScriptのプログラムを格納するバケット
  2. 生成したテストコードを格納するバケット

AWS管理コンソールから「S3」の画面を開き、「バケットの作成」を選択します。
「バケット作成」の画面でバケット名を入力してください。
その他の設定は初期設定のまま、「バケットを作成」を選択してください。

上記の画面で以下2つのバケットを作成します。

  1. 作成したTypeScriptのプログラムを格納するバケット
  2. 生成したテストコードを格納するバケット

バケット名は、基本的にすべての AWS リージョンのすべてのAWSアカウントで一意となります。重複している場合は登録できないのでご注意ください。
命名規約について、より詳細の内容を確認したい場合は以下をご確認ください。
バケットの名前付け

バケットの作成が完了するとバケットの一覧に作成したバケットが表示されます。

S3バケットの作成はこれで完了です。

2. Lambda関数の作成

「作成したTypeScriptのプログラムを格納するバケット」のファイルがアップロードされると起動するLambda関数を作成します。
このLambda関数では、アップロードされたプログラムを読み込み、Bedrockにテストコードの生成を指示し、生成したテストコードを「生成したテストコードを格納するバケット」にアップロードします。

AWS管理コンソールから「Lambda」の画面を開き、「関数の作成」を選択します。
「関数の作成」の画面が表示されます。関数名を入力し、「関数の作成」を選択してください。

関数が作成されます。デフォルトのタイムアウト値だと、処理が終わる前にタイムアウトしてしまうため設定を変更します。
「設定」タブを選択し、左メニューの「一般設定」を選択、編集ボタンを選択してください。

「タイムアウト」を3秒から1分に変更し、「保存」を選択します。

環境変数を設定します。「設定」タブを選択し、左メニューの「環境変数」を選択、編集ボタンを選択してください。

以下の環境変数を追加します。追加した後に「保存」を選択してください。

キー 説明
REGION 【リージョン名】 S3バケットと同じリージョン
ao-northeast-1など
MODELID anthropic.claude-3-5-sonnet-20240620-v1:0 投稿時点での最新モデルを指定しています。
VERSION bedrock-2023-05-31 anthropicのバージョン。公式ドキュメントを見ると「bedrock-2023-05-31」を指定する旨が記載されています。
BUCKET_NAME 【出力先のバケット名】 「1. S3バケットの作成」で作成した
「生成したテストコードを格納するバケット」のバケット名

Lambda関数で実行するコードを入力していきます。
「コード」タブを選択し、「EXPLORER」の「index.mjs」を「index.js」にリネームし、コードを以下の内容で更新し、「Deploy」を選択してください。

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handler = void 0;
const client_s3_1 = require("@aws-sdk/client-s3");
const client_bedrock_runtime_1 = require("@aws-sdk/client-bedrock-runtime");
const stream_1 = require("stream");
// 環境変数の取得
const REGION = process.env.REGION;
const MODELID = process.env.MODELID;
const VERSION = process.env.VERSION;
const BUCKET_NAME = process.env.BUCKET_NAME;
// 変数定義
const basePrompt = () => {
    return `
      # 指示文
      - 以下の条件に基づいて入力されたTypeScriptのコードのテストケースとテストコードを生成してください。

      # 条件
      - 入力されたコードが不正な場合は、resultはfalseとしてください。正常な場合はtrueとしてください。
      - テストケースは以下の項目を出力してください。()内は説明のため出力時には不要です。
      - テスト項目件名(テストの件名)
      - テスト条件(インプットのパラメタやテスト環境の条件設定)
      - テスト結果(実行した結果の予想される復帰値やメッセージ json形式ではなく文章でわかるようにしてください。)
      - 分岐網羅 (C1カバレッジ)でテストケースを生成してください。
      - テストコードはJestを使ってください
      - 処理内で関数を使う場合、mockを使ってください。
      - テストケースで洗い出しをした項目をテストコードとしてください。
      - 結果はJSONの出力形式で復帰してください。その他のメッセージは不要です。
      - エスケープが必要な文字列はエスケープしてください。
      - NodeJsのSON.parseを使って文字列をオブジェクト化するのでparseできるように文字列をエスケープしてください。
      - jsonのキーと値は"key": "value"といったように「"」でくくってください。
      - json値に「"」は使わず、「'」を使ってください。例)"key": "value: 'message text'"
      - テストコード内の改行コード(\n)はjson形式で識別できるように「\\n」で出力してください。

      # 出力形式
      {
        "result": 【true or false】
        "items": [
          {
            "title": "【テスト項目件名】",
            "condition": "【テスト条件】",
            "prediction": "【テスト結果】",
          },
          ・
          ・
          ・
        ],
        "code": "【テストコード】"
      }
    `;
};
// Clientの初期化
const s3Client = new client_s3_1.S3Client({ region: REGION });
const bedrockRuntimeClient = new client_bedrock_runtime_1.BedrockRuntimeClient({ region: REGION });
// handler関数の定義
const handler = async (event) => {
    try {
        // インプットチェック
        console.log("----- インプットチェック -----");
        if (!(event &&
            event.Records &&
            event.Records.length > 0 &&
            event.Records[0].s3 &&
            event.Records[0].s3.bucket &&
            event.Records[0].s3.bucket.name &&
            event.Records[0].s3.object &&
            event.Records[0].s3.object.key)) {
            throw new Error("eventの構造が不正です");
        }
        const inputBucketName = event.Records[0].s3.bucket.name;
        const inputObjectKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
        const inputFileName = inputObjectKey.split('/').pop() || '';
        const lastDotIndex = inputFileName.lastIndexOf('.');
        let fileName = '';
        let extension = '';
        if (lastDotIndex === -1) {
            // 拡張子が存在しない場合
            fileName = inputFileName;
            extension = '';
        }
        else {
            fileName = inputFileName.substring(0, lastDotIndex);
            extension = inputFileName.substring(lastDotIndex + 1);
        }
        if (extension != 'ts') {
            throw new Error("拡張子が不正でkす");
        }
        console.log("----------");
        // オブジェクトの読み込み
        console.log("----- オブジェクトの読み込み -----");
        const getObjectParams = {
            Bucket: inputBucketName,
            Key: inputObjectKey,
        };
        const getObjectCommand = new client_s3_1.GetObjectCommand(getObjectParams);
        const s3Response = await s3Client.send(getObjectCommand);
        console.log("----------");
        // 読み込んだオブジェクトのチェック
        console.log("----- 読み込んだオブジェクトのチェック -----");
        if (!s3Response.Body || !(s3Response.Body instanceof stream_1.Readable)) {
            throw new Error("アップロードされたオブジェクトが不正です");
        }
        const chunks = [];
        for await (const chunk of s3Response.Body) {
            chunks.push(chunk);
        }
        const fileContent = Buffer.concat(chunks).toString('utf-8');
        if (!fileContent) {
            throw new Error("アップロードされたテキストが空白です。");
        }
        if (fileContent.length > 5000) {
            throw new Error("解析するプログラム5000文字以内にしてください。");
        }
        console.log("解析するプログラム:\n" + fileContent);
        console.log("----------");
        // Bedrock実行パラメタの定義
        console.log("----- Bedrock実行パラメタの定義 -----");
        const payload = {
            anthropic_version: VERSION,
            max_tokens: 5000,
            system: basePrompt(),
            messages: [
                {
                    role: "user",
                    content: [
                        { type: "text", text: fileContent }
                    ],
                }
            ],
        };
        console.log("payload:");
        console.log(payload);
        console.log("----------");
        // Claudeで実行
        console.log("----- Claudeで実行 -----");
        const command = new client_bedrock_runtime_1.InvokeModelCommand({
            contentType: "application/json",
            body: JSON.stringify(payload),
            accept: "application/json",
            modelId: MODELID,
        });
        console.log("command:");
        console.log(command);
        const response = await bedrockRuntimeClient.send(command);
        console.log("----------");
        // Bedrock復帰処理
        console.log("----- Bedrock復帰処理 -----");
        const decodedResponseBody = new TextDecoder().decode(response.body);
        const responseBody = JSON.parse(decodedResponseBody);
        console.log("responseBody: ");
        console.log(responseBody);
        console.log("responseBody.content[0].text.replace: ");
        console.log(responseBody.content[0].text.replace(/\\n/g, "\\\\n"));
        const contentTextJson = JSON.parse(responseBody.content[0].text
            .replace(/\\n/g, "\\\\n"));
        console.log("contentTextJson: ");
        console.log(contentTextJson);
        console.log("----------");
        // テスト項目確認
        console.log("----- テスト項目・テストコード確認 -----");
        if (!(contentTextJson &&
            contentTextJson.result &&
            contentTextJson.items &&
            contentTextJson.items.length > 0 &&
            contentTextJson.code)) {
            console.log(contentTextJson);
            console.log(contentTextJson.result);
            console.log(contentTextJson.items);
            console.log(contentTextJson.items.length > 0);
            console.log(contentTextJson.code);
            throw new Error("contentTextJsonの構造が不正です");
        }
        console.log("----------");
        // テスト項目json→csv変換
        console.log("----- テスト項目json→csv変換 -----");
        const testItems = contentTextJson.items;
        const headers = ['title', 'condition', 'prediction'];
        const csvRows = [headers.join(', ')];
        testItems.forEach((item) => {
            const row = [item.title, item.condition, item.prediction];
            csvRows.push(row.join(', '));
        });
        const csvData = csvRows.join('\n');
        console.log("----------");
        // テストコード改行コード変換
        console.log("----- テストコード改行コード変換 -----");
        const testCode = contentTextJson.code.replace(/\\\\n/g, "\n");
        console.log("testCode: ");
        console.log(testCode);
        console.log("----------");
        // S3にアップロード
        console.log("----- S3にアップロード -----");
        const testItemsKey = 'testItems/' + fileName + '.csv';
        const testCodeKey = 'testcode/' + fileName + '.spec.ts';
        const uploadTestItemsParams = {
            Bucket: BUCKET_NAME,
            Key: testItemsKey,
            Body: csvData,
            ContentType: "text/csv",
        };
        await s3Client.send(new client_s3_1.PutObjectCommand(uploadTestItemsParams));
        const uploadTestCodeParams = {
            Bucket: BUCKET_NAME,
            Key: testCodeKey,
            Body: contentTextJson.code.replace(/\\n/g, "\n"),
            ContentType: "text/ts",
        };
        await s3Client.send(new client_s3_1.PutObjectCommand(uploadTestCodeParams));
        console.log("----------");
    }
    catch (error) {
        console.error("Error invoking Bedrock model:", error);
    }
};
exports.handler = handler;

続いてS3のファイルアップロードをトリガーに、Lambda関数を起動する設定をします。
「トリガーの追加」を選択します。

以下を入力し、追加を選択します。

項目 説明
トリガーの設定 S3 S3バケットのイベントをトリガーとします。
バケット 【作成したTypeScriptのプログラムを格納するバケット】 「作成したTypeScriptのプログラムを格納するバケット」で作成したバケット入力してください。
イベントタイプ PUT S3のファイルアップロード(PUT)イベントのみ設定
プレフィックス – オプション 設定なし
サフィックス – オプション .ts typescriptのファイルを指定
再帰呼び出し チェックする 今回は違うバケットに出力するため、問題ありませんが、
インプットのS3バケット=アウトプットのS3バケットとなる場合は、無限ループになるため注意が必要です。
Lambdaが無限に動作するため、Lambda/S3の利用料が高額になる可能性があります。

トリガーにS3が追加されています。

これでLambda関数の作成は完了です。

3. Lambda関数のロール設定

Lambda関数に紐づいているIAMロールに必要な許可を追加します。
Lambdaに紐づいているIAMロールの画面を表示するために、Lambda関数の「設定」タブを選択し、左メニューから「一般設定」を選択し、「編集」を選択します。
※実際にLambda関数の設定は修正しません。

基本設定の画面が表示されます。画面下部にIAMロールのリンクがあるので、選択してください。

IAMロール画面が表示されます。「許可を追加」から「インラインポリシーを作成」を選択します。

設定画面が表示されます。ポリシーエディタの設定を「JSON」に切り替えてください。

以下を入力してください。
arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0」の部分は利用するリージョンやモデルIDに合わせて変更してください。
入力後、画面下の「次へ」を選択してください。

{
    "Version": "2012-10-17",
    "Statement": [
	{
		"Effect": "Allow",
		"Action": "s3:GetObject",
		"Resource": "arn:aws:s3:::【作成したTypeScriptのプログラムを格納するバケット】/*.ts"
	},
	{
		"Effect": "Allow",
		"Action": "s3:PutObject",
		"Resource": "arn:aws:s3:::【生成したテストコードを格納するバケット】/*"
	},
        {
            "Effect": "Allow",
            "Action": "bedrock:InvokeModel",
            "Resource": [
                "arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"
            ]
        }
    ]
}

ポリシー名を入力して、「ポリシーの作成」を選択してください。

作成したポリシーが追加されます。

これで「Lambda関数のロール設定」は完了です。

動作確認

「作成したTypeScriptのプログラムを格納するバケット」にTypeScriptのプログラムをアップロードします。
アップロードするとアップロードイベントをトリガーにLambda関数が動作し、アップロードしたファイルの取得、プロンプトの実行、結果のアップロードが実行されます。

利用するプログラムは「CodeBuildを使ってビルド・テストをしてみる」で使ったTypeScriptのプログラムをインプットとします。

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const s3Client = new S3Client({ region: process.env.AWS_REGION });
const BUCKET_NAME = process.env.BUCKET_NAME;

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
    try {
        // Validate query parameters
        const { queryStringParameters } = event;
        if (!queryStringParameters) {
            return {
                statusCode: 400,
                body: JSON.stringify({ message: "Missing query parameters" }),
            };
        }

        const { filename, content } = queryStringParameters;

        if (!filename || !content) {
            return {
                statusCode: 400,
                body: JSON.stringify({ message: "Both 'filename' and 'content' parameters are required" }),
            };
        }

        // Upload content to S3
        const uploadParams = {
            Bucket: BUCKET_NAME,
            Key: filename,
            Body: content,
            ContentType: "text/plain",
        };

        await s3Client.send(new PutObjectCommand(uploadParams));

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: "File uploaded successfully",
                filename,
            }),
        };
    } catch (error) {
        console.error("Error uploading file to S3:", error);

        return {
            statusCode: 500,
            body: JSON.stringify({
                message: "Internal Server Error",
                error: error instanceof Error ? error.message : "Unknown error",
            }),
        };
    }
};

ファイルをアップロードしてみます。

しばらくすると、「生成したテストコードを格納するバケット」にファイルが生成されています。

テストケースがcsvで出力されています。

以下csvの内容を表にしたものです。

テストケース テスト条件 テスト予想結果
正常系: ファイルのアップロードが成功する 有効なfilenameとcontentパラメータが提供される ステータスコード200とファイルのアップロード成功メッセージが返される
異常系: クエリパラメータが欠落している クエリパラメータが提供されない ステータスコード400と’Missing query parameters’メッセージが返される
異常系: filenameパラメータが欠落している contentパラメータのみ提供される ステータスコード400と’Both ‘filename’ and ‘content’ parameters are required’メッセージが返される
異常系: contentパラメータが欠落している filenameパラメータのみ提供される ステータスコード400と’Both ‘filename’ and ‘content’ parameters are required’メッセージが返される
異常系: S3へのアップロードでエラーが発生 S3クライアントがエラーをスローする ステータスコード500と’Internal Server Error’メッセージが返される

続いてテストコードです。

テスト内容はcsvの内容と一致してますね。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { handler } from './your-handler-file';

jest.mock('@aws-sdk/client-s3');

describe('S3 Upload Lambda Function', () => {
  const mockS3Send = jest.fn();
  S3Client.prototype.send = mockS3Send;

  beforeEach(() => {
    jest.clearAllMocks();
    process.env.BUCKET_NAME = 'test-bucket';
  });

  test('正常系: クエリパラメータが正しく設定されている場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt',
        content: 'Hello, World!'
      }
    };

    mockS3Send.mockResolvedValue({});

    const response = await handler(event as any);

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual({
      message: 'File uploaded successfully',
      filename: 'test.txt'
    });
    expect(mockS3Send).toHaveBeenCalledWith(
      expect.objectContaining({
        input: expect.objectContaining({
          Bucket: 'test-bucket',
          Key: 'test.txt',
          Body: 'Hello, World!',
          ContentType: 'text/plain'
        })
      })
    );
  });

  test('異常系: クエリパラメータが存在しない場合', async () => {
    const event = {};

    const response = await handler(event as any);

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Missing query parameters'
    });
    expect(mockS3Send).not.toHaveBeenCalled();
  });

  test('異常系: filename パラメータが欠落している場合', async () => {
    const event = {
      queryStringParameters: {
        content: 'Hello, World!'
      }
    };

    const response = await handler(event as any);

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Both 'filename' and 'content' parameters are required'
    });
    expect(mockS3Send).not.toHaveBeenCalled();
  });

  test('異常系: content パラメータが欠落している場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt'
      }
    };

    const response = await handler(event as any);

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Both 'filename' and 'content' parameters are required'
    });
    expect(mockS3Send).not.toHaveBeenCalled();
  });

  test('異常系: S3へのアップロードでエラーが発生した場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt',
        content: 'Hello, World!'
      }
    };

    mockS3Send.mockRejectedValue(new Error('S3 Upload Error'));

    const response = await handler(event as any);

    expect(response.statusCode).toBe(500);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Internal Server Error',
      error: 'S3 Upload Error'
    });
    expect(mockS3Send).toHaveBeenCalled();
  });
});

構文エラーや動かないテストコードがあるので、手修正します。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { APIGatewayProxyHandlerV2, APIGatewayProxyResult, Context } from 'aws-lambda';
import { handler } from './../src/handler';

jest.mock("@aws-sdk/client-s3", () => {
  const mockS3Client = {
      send: jest.fn(),
  };
  return {
      S3Client: jest.fn(() => mockS3Client),
      PutObjectCommand: jest.fn((params) => params),
  };
});

describe('S3 Upload Lambda Function', () => {
  const mockS3Client = new S3Client({});

  const mockContext: Context = {
    awsRequestId: "test-request-id",
    callbackWaitsForEmptyEventLoop: false,
    functionName: "test-function",
    functionVersion: "$LATEST",
    invokedFunctionArn: "arn:aws:lambda:us-east-1:123456789012:function:test-function",
    logGroupName: "/aws/lambda/test-function",
    logStreamName: "test-log-stream",
    memoryLimitInMB: "128",
    getRemainingTimeInMillis: () => 3000,
    done: () => {},
    fail: () => {},
    succeed: () => {},
} as Context;

  beforeEach(() => {
    jest.clearAllMocks();
    process.env.BUCKET_NAME = 'test-bucket';
  });

  test('正常系: クエリパラメータが正しく設定されている場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt',
        content: 'Hello, World!'
      }
    } as any;

    (mockS3Client.send as jest.Mock).mockImplementation((command) => {
      return Promise.resolve(command.input);
    });

    const response = (await handler(event, mockContext, () => {})) as APIGatewayProxyResult;

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual({
      message: 'File uploaded successfully',
      filename: 'test.txt'
    });
    expect(mockS3Client.send).toHaveBeenCalledWith(
      expect.objectContaining({
        Bucket: 'test-bucket',
        Key: 'test.txt',
        Body: 'Hello, World!',
        ContentType: 'text/plain'
      })
    );
  });

  test('異常系: クエリパラメータが存在しない場合', async () => {
    const event = {} as any;

    const response = (await handler(event, mockContext, () => {})) as APIGatewayProxyResult;

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Missing query parameters'
    });
    expect(mockS3Client.send).not.toHaveBeenCalled();
  });

  test('異常系: filename パラメータが欠落している場合', async () => {
    const event = {
      queryStringParameters: {
        content: 'Hello, World!'
      }
    } as any;

    const response = (await handler(event, mockContext, () => {})) as APIGatewayProxyResult;

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Both \'filename\' and \'content\' parameters are required'
    });
    expect(mockS3Client.send).not.toHaveBeenCalled();
  });

  test('異常系: content パラメータが欠落している場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt'
      }
    } as any;

    const response = (await handler(event, mockContext, () => {})) as APIGatewayProxyResult;

    expect(response.statusCode).toBe(400);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Both \'filename\' and \'content\' parameters are required'
    });
    expect(mockS3Client.send).not.toHaveBeenCalled();
  });

  test('異常系: S3へのアップロードでエラーが発生した場合', async () => {
    const event = {
      queryStringParameters: {
        filename: 'test.txt',
        content: 'Hello, World!'
      }
    } as any;

    (mockS3Client.send as jest.Mock).mockRejectedValue(new Error('S3 Upload Error'));

    const response = (await handler(event, mockContext, () => {})) as APIGatewayProxyResult;

    expect(response.statusCode).toBe(500);
    expect(JSON.parse(response.body)).toEqual({
      message: 'Internal Server Error',
      error: 'S3 Upload Error'
    });
    expect(mockS3Client.send).toHaveBeenCalled();
  });
});

こちらでテストを実行してみます。以下の結果となりました。

分岐網羅(C1カバレッジ)で指定していましたが、1分岐だけ漏れがあるようです。
生成AIなので何回か実行すると結果が変わるかと思い、5回試行してみましたが結果は変わりませんでした。

とはいえ、0の状態からテストケースやテストコードを生成してくれるのは、かなり手間が省けるかと思います。

まとめ

本記事ではTypeScriptのプログラムをインプットとして、生成AIを使ってテストケースやJestのテストコードの生成をしてみました。
プロンプトに条件を指定することで条件にあったテストケースやテストコードの大枠は作成できました。
今回の結果からみると、テストケースの不足があったり、テストコードの手直しが必要でしたが、0から作成するよりは、手間が省けて便利だなといった感想でした。
プロンプト次第で改善ができたり、他の使い方も可能なので、本記事の内容を参考にしてみてください。

タイトルとURLをコピーしました