Closed18

関数渡しただけでテスト作ってくれるGPTsが欲しい

hajimismhajimism

一旦こんな感じで

# instruction
受け取ったコードスニペットの関数に対して、テストを書いてください。
その際、ただテストを書くだけでなく、下記を事前に整理して記述してください。
- その関数の役割やインターフェイスに関する分析
- (もしあれば)よりtestableな関数にするためのリファクタリング案
- その関数に対して実施するべきテストの内容とその理由の要約(箇条書き)

## role
プロのQAエンジニアとして振る舞ってください。
回答はテストを書き慣れていないジュニアエンジニアに対してレクチャーするかのように記述して下さい。

## output
英語で思考して、日本語で出力してください。

出力形式の凡例を以下に記します。
`\`\`
# 関数の役割やインターフェイスに関する分析

(関数の役割やインターフェイスに関する分析をここに書く)

# リファクタリング案

(よりtestableな関数にするためのリファクタリング案をここに書く。なければ「特になし」と書く)

# テスト内容とその理由

(その関数に対して実施するべきテストの内容とその理由の要約を箇条書きで書く)

# テストコード

(その関数に対する、プロの現場で使われるような高い品質のテストコードを書く。上で記述した「テストの内容」を網羅するように書く。)
`\`\`

大変なお仕事かと思いますが、がんばってください。めちゃくちゃ応援しています。
hajimismhajimism

const sum = (a: number, b: number) => a + bの入力に対してこう返してきた

# 関数の役割やインターフェイスに関する分析

この関数「sum」は、二つの数値(aとb)を受け取り、それらの合計を返すシンプルな関数です。引数としてnumber型のaとbを取り、戻り値もnumber型です。

# リファクタリング案

特になし。この関数はすでにシンプルでテストしやすい形になっています。

# テスト内容とその理由

1. 正数の加算: 一般的な使用例であり、基本的な加算が正しく行われるかを確認する。
2. 負数の加算: 負の数値が正しく加算されるかを確認する。
3. ゼロの加算: 0が加算の際に期待通りの挙動を示すかを確認する。
4. 浮動小数点数の加算: 浮動小数点数の取り扱いが正確であるかをテストする。
5. 大きな数値の加算: 大きな数値が正しく加算されるかを確認する。
6. 型の異なる引数の扱い: TypeScriptはコンパイル時に型チェックを行うため、このテストは実行時ではなく静的解析のレベルで必要です。

# テストコード

以下はTypeScriptで書かれたJestフレームワークを使ったテストコードの例です。

`\`\`typescript
describe('sum function', () => {
  test('adds two positive numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('adds two negative numbers', () => {
    expect(sum(-1, -2)).toBe(-3);
  });

  test('adds zero', () => {
    expect(sum(0, 0)).toBe(0);
    expect(sum(0, 1)).toBe(1);
    expect(sum(1, 0)).toBe(1);
  });

  test('adds floating point numbers', () => {
    expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
  });

  test('adds large numbers', () => {
    expect(sum(1000000, 1000000)).toBe(2000000);
  });
});

`\`\`
hajimismhajimism
# constraint
- ユーザーがコピーしやすいように、テストコードのスニペットだけをコードブロックで囲んでください。その他は、出力形式の凡例に則りつつ平文で書いてください。

っていうのを足してみる

hajimismhajimism

いい感じに返してくれた

# 関数の役割やインターフェイスに関する分析

この関数は二つの数値(a と b)を引数として受け取り、それらの和を返します。TypeScriptを使用しているため、引数と戻り値は数値型(number)であることが保証されています。

# リファクタリング案

特になし。この関数は非常にシンプルで、テストに必要な要件をすでに満たしています。

# テスト内容とその理由

- 正の数の加算: 正の数同士が正しく加算されるかをテストします。基本的な機能性を確認するため。
- 負の数の加算: 負の数同士が正しく加算されるかをテストします。エッジケースを扱えるかを確認するため。
- 正の数と負の数の加算: 正の数と負の数が正しく加算されるかをテストします。異なる種類の入力が適切に処理されるかを確認するため。
- ゼロの加算: ゼロと他の数値の加算が正しく行われるかをテストします。特別な値(ゼロ)の取り扱いを確認するため。
- 大きな数値の加算: JavaScriptの数値限界を超えない範囲での大きな数値の加算をテストします。数値の限界値近くでの正確性を確認するため。

# テストコード

(筆者後述)

このテストコードはJestなどのJavaScriptテスティングフレームワークを使用しています。テストケースは関数の基本的な動作を網羅しており、さまざまな種類の入力に対する正しい振る舞いを保証します。また、TypeScriptを使用しているため、型に関するテストは不要です。
hajimismhajimism

テストコード

import { sum } from './sum'; // 関数のパスに応じて変更してください

describe('sum function', () => {
  it('adds two positive numbers correctly', () => {
    expect(sum(5, 3)).toBe(8);
  });

  it('adds two negative numbers correctly', () => {
    expect(sum(-5, -3)).toBe(-8);
  });

  it('adds a positive and a negative number correctly', () => {
    expect(sum(-5, 3)).toBe(-2);
  });

  it('adds zero correctly', () => {
    expect(sum(0, 5)).toBe(5);
    expect(sum(5, 0)).toBe(5);
  });

  it('handles large numbers correctly', () => {
    expect(sum(1000000, 1000000)).toBe(2000000);
  });
});

hajimismhajimism

const Component = () => <h1>hello</h1>

に対して

# 関数の役割やインターフェイスに関する分析

こちらのコードは、Reactの関数コンポーネントを定義しています。このコンポーネントは、単純な機能を持ち、<h1> タグで "hello" というテキストを表示します。インターフェイスとしては、このコンポーネントはpropsを受け取らず、外部からの依存もないため、比較的単純な構造をしています。

# リファクタリング案
特になし。このコンポーネントは既にシンプルであり、テストしやすい状態です。

# テスト内容とその理由

- レンダリングテスト: コンポーネントが正しくレンダリングされるかどうかをテストします。これは基本的な機能テストであり、コンポーネントの基本的な健全性を確認するために必要です。
- コンテンツの検証: コンポーネントが <h1> タグ内に "hello" というテキストを表示していることを確認します。これはコンポーネントが期待通りの出力をしているかを確認するために重要です。

# テストコード
以下に、Reactのテストツールである react-testing-library を使用したテストコードの例を示します。このライブラリはReactコンポーネントのテストに適しており、自然な方法でDOMをクエリしてテストを行うことができます。

(筆者後述)

このテストコードでは、まず Component をレンダリングし、それから screen.getByRole メソッドを使用してDOMに描画された要素を取得し、その要素が期待通りのものであるかを expect を使って検証しています。これにより、コンポーネントが正しくレンダリングされ、正しい内容を表示していることが確認できます。
hajimismhajimism

テストコード

import { render, screen } from '@testing-library/react';
import Component from './Component';

describe('Component', () => {
  test('should render without crashing', () => {
    render(<Component />);
    expect(screen.getByRole('heading')).toBeInTheDocument();
  });

  test('should display the correct text', () => {
    render(<Component />);
    expect(screen.getByRole('heading')).toHaveTextContent('hello');
  });
});

hajimismhajimism
fn sum(a: i32, b: i32) -> i32 {
    a + b
}

に対しては

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_positive_numbers() {
        assert_eq!(sum(2, 3), 5);
    }

    #[test]
    fn test_negative_numbers() {
        assert_eq!(sum(-4, -6), -10);
    }

    #[test]
    fn test_positive_and_negative() {
        assert_eq!(sum(-5, 3), -2);
    }

    #[test]
    fn test_zero() {
        assert_eq!(sum(0, 0), 0);
        assert_eq!(sum(0, 5), 5);
        assert_eq!(sum(3, 0), 3);
    }

    // オーバーフローのテストは、Rustの標準動作ではpanicを引き起こすので、
    // このテストはデフォルトではコメントアウトされています。
    // 実行する場合は、適切な環境でのみ行ってください。
    /*
    #[test]
    #[should_panic]
    fn test_overflow() {
        sum(i32::MAX, 1);
    }
    */
}

と返ってきた

hajimismhajimism

当然だけど特定言語やフレームワークに特化して、制約を色々加えたほうが品質は上がりそう

hajimismhajimism

読みづらい関数を渡してみたが

export const truncateAddress = (address: string) => {
  if (!address) return 'No Account'
  const match = address.match(/^(0x[a-zA-Z0-9]{6})[a-zA-Z0-9]+([a-zA-Z0-9]{4})$/)
  if (!match) return address
  return `${match[1]}${match[2]}`
}

そんなにリファクタリングを提案されなかった

関数の役割やインターフェイスに関する分析
truncateAddress関数は、文字列として渡されたアドレスを短縮形に変換する役割を持っています。主に以下の条件に基づいて動作します。

引数が空の場合、'No Account'を返します。
引数が特定のパターン(0xで始まる、中間の文字列を省略し、最後に4文字を残す)にマッチしない場合は、そのままのアドレスを返します。
パターンにマッチする場合、省略形のアドレスを生成して返します。
リファクタリング案
特になし。関数はすでにその目的に適した単純なインターフェースを持っています。ただし、正規表現が複雑であるため、それを解析する際のコメントを追加することが推奨されます。

テスト内容とその理由
空の入力: 空の文字列が与えられた場合に'No Account'が返されることを確認します。
無効なフォーマット: パターンにマッチしないアドレスがそのまま返されることを確認します。
有効なフォーマット: 正しいフォーマットのアドレスが適切に短縮されることを確認します。
ケースセンシティブチェック: アドレスの大文字小文字が正しく処理されることを確認します。
hajimismhajimism

「リファクタリングの余地がある場合は、改善後のコード例を示してくれ」と頼んだらそうしてくれた。

リファクタリング案
関数は既にシンプルでテストしやすい形になっていますが、一点改善できる部分があります。それは、正規表現がマジックナンバー(直接的な数字の記述)を含んでいる点です。この正規表現を、より読みやすくメンテナンスしやすい形にすることができます。
export const truncateAddress = (address: string) => {
  if (!address) return 'No Account'
  const prefixLength = 6;
  const suffixLength = 4;
  const regex = new RegExp(`^(0x[a-zA-Z0-9]{${prefixLength}})[a-zA-Z0-9]+([a-zA-Z0-9]{${suffixLength}})$`);
  const match = address.match(regex);
  if (!match) return address;
  return `${match[1]}${match[2]}`;
}

hajimismhajimism

だけどテストコードにchaiやmochaを使ってきた。限定したい。

import { truncateAddress } from './your-module';
import { expect } from 'chai';
import 'mocha';
hajimismhajimism

ユーザーの入力では特定のテスト用ライブラリが指定されている場合があります。もし指定されていたらそのライブラリを用いたテストコードを記述してください。指定されていなければ、その言語やフレームワークに対して最もメジャーであると考えられるライブラリを用いたテストコードを記述してください。

という記述を追加したら

JavaScriptのテストにはJestが広く使われているため、Jestを用いたテストコードを記述します。

と答えてきた

hajimismhajimism

リファクタリング案とか吐き出すテストコードについては結構まちまち

hajimismhajimism

リファクタリング観点は大きく分けてreadabilityとtestabilityがあると思うので、明示的にそれを分けるように指示した。

hajimismhajimism

ChatGPTに敢えて駄目な関数を書いてもらって、

function processDataAndLog(data: any[]): void {
    // データを処理
    let processedData = data.map(d => ({...d, processed: true}));
    // 処理されたデータをロギング
    console.log("Processed Data:", processedData);
    // データをデータベースに保存(仮)
    // database.save(processedData);
}

それを渡してみた

## 関数の役割やインターフェイスに関する分析

この関数processDataAndLogは、引数として受け取った配列data内の各要素に対して処理を行い(ここでは要素にprocessed: trueを追加)、処理されたデータをコンソールにログ出力し、最後にこれらをデータベースに保存する処理を行う(現在はコメントアウトされています)。この関数は副作用(ログの出力とデータベースへの保存)を伴います。

## リファクタリング案

### 可読性向上の余地
関数の単一責任原則の適用: 現在の関数は、データの処理、ログの出力、データベースへの保存と複数の責任を持っています。これを複数の関数に分割することで可読性とメンテナンス性を向上させることができます。
改善後のコード例:
function processDataset(data: any[]): any[] {
    return data.map(d => ({ ...d, processed: true }));
}

function logProcessedData(data: any[]): void {
    console.log("Processed Data:", data);
}

function saveProcessedData(data: any[]): void {
    // database.save(data);
}
### テスタビリティ向上の余地
副作用の分離: ロギングとデータベース保存の機能を別の関数に分割することで、データ処理のロジックを単独でテストできるようになります。
依存関係の注入: データベースやロギング機能への依存を外部から注入できるようにすることで、テスト時にモックやスタブを使用しやすくなります。

改善後のコード例(上記のコードを踏まえた):

(空振りしてた)

## テスト内容とその理由
- データ処理の正確性: processDataset関数がすべての要素にprocessed: trueを正しく追加しているかを確認する。
- ログ出力の確認: logProcessedData関数が適切なデータをログに出力しているかどうかを確認する。ただし、ログ出力は副作用なので、実際のコンソールへの出力をテストするのではなく、ログ出力が行われたかの確認に留める。
- データベース保存の確認: saveProcessedData関数がデータをデータベースに保存する処理を呼び出しているかを確認する。この部分も、実際のデータベースへの保存をシミュレートするのではなく、保存処理が呼び出されているかをモックで確認する。

## テストコード
JavaScriptのテストフレームワークとしてはJestが広く使われています。以下はJestを使用したテストコードの例です。
describe('Data processing functions', () => {
    it('processDataset adds processed: true to every item', () => {
        const sampleData = [{ id: 1 }, { id: 2 }];
        const processed = processDataset(sampleData);
        expect(processed).toEqual([{ id: 1, processed: true }, { id: 2, processed: true }]);
    });

    it('logProcessedData calls console.log with correct data', () => {
        const consoleSpy = jest.spyOn(console, 'log');
        const sampleData = [{ processed: true }];
        logProcessedData(sampleData);
        expect(consoleSpy).toHaveBeenCalledWith('Processed Data:', sampleData);
    });

    // Note: This test assumes saveProcessedData function is modified to accept a database object
    it('saveProcessedData calls database save method', () => {
        const mockDatabase = { save: jest.fn() };
        const sampleData = [{ processed: true }];
        saveProcessedData(sampleData, mockDatabase);
        expect(mockDatabase.save).toHaveBeenCalledWith(sampleData);
    });
});
このスクラップは2023/11/15にクローズされました