ChatGPT に 1分間でユニットテストを書いて貰って、5時に退社しよう

投稿者: | 2023年8月7日

Let’s have ChatGPT write a unit test in one minute.

相模原市で IoT 設計を受託しているファームロジックスです。

前回に続き今回も、ChatGPT を活用してソフトウェア技術者が仕事の生産性を上げていこう、というお話です。

ここ20年でソフトウェア技術者の環境はどう変わったか?

いつも前置きが長くてすみません。

1990年代、身近でコンピュータ(パソコン)が利用できるようになった頃、野口悠紀雄先生など知識人の方々が、データの検索のような、人には退屈で機械のほうが上手にできる仕事は、機械にまかせよう、という記事を盛んに書いていらっしゃいました。(「超」整理法の続編シリーズでしたでしょうか。)

その後、2000年代にかけて Google を筆頭にインターネット検索が普及し始めました。もの知りなだけの人は仕事を失い、情報の選別・評価能力、独自のアプローチで問題解決策を見つけられる人の価値が高まるだろう、と言われるようになりました。現在でも、ソフトウェア技術者は Google → StackOveflow な毎日ではないでしょうか。

そして 2023年となり、機械学習(ML)技術に基づく AI を誰もが容易に利用できるようになりました。数年前まで、ML と言えば TensorFlow など高度な技術を駆使できる人だけが ML の恩恵にあずかれましたが、今やそうでない人でも ML、AI の技術を簡単に利用できるようになったのです。

単体テスト、してますか?

ソフトウェア技術者の皆さんにとって、ソフトウェアのテストというのは、こそばゆいキーワードなのではないでしょうか。

ソフトウェアテストの重要性は盛んに強調されながら、多くの設計現場で、ドキュメンテーションと同じくらい、テストは常に後回しにされがちな作業です。スキルある多くの管理者の方々が、プロジェクト成功の成否は、テストやドキュメンテーションにかかっていると強調していても、担当者にはそれほどの余裕がないことが多いでしょう。

特に、結合テストは実施していても、単体テストをしっかりやっているソフトウェア設計プロジェクトは少数派なのではないかと思います。ソフトウェアプロジェクト管理手法を書いた本をひもとくと、コードを書く前にテストを書け、などと書かれてますが、実際に、そこまで管理がしっかりしたプロジェクトがどれだけあるでしょうか。

ソフトウェア設計に伴う、退屈で煩雑な仕事は AI に任せよう

なぜ、単体テストが省略されがちなのかと言えば、単体テストのテストコードを書くのは退屈で、面倒で、とっかかりのタイミングを得にくいからです。何か設計上のアイデアがあれば、まずコードを書いてアイデアを試したくなるのが正直なところでしょう。

そんなときに、単体テストを先に書いておこう、というのは、管理者の理解に恵まれた、かつ、神様みたいに優秀なソフトウェア設計者だと思います。

我々は神様ではないので、単体テストをじっくり書いている余裕はありません。そんな仕事こそ、AI にやって貰おうではありませんか!

まずは何か関数を書いて貰おう

早速、ChatGPT(GPT-4)を開きましょう。今回は、7月頃にリリースされた Code Interpreter にも働いて貰うことになるので、Code Interpreter をオンにしておきます。

いま仮に、次のような関数を設計しなくてはならないとします。ChatGPT の対話形式で進めます。

: 2023年8月5日、とか、2023/8/5などのテキストを、Pythonの datetime 型に変換する関数を書いてください。

ChatGPT は瞬く間に、つぎのようなコードを提示してくれました。

from datetime import datetime

def str_to_datetime(date_str: str) -> datetime:
    # 2023年8月5日 形式
    if "年" in date_str and "月" in date_str and "日" in date_str:
        return datetime.strptime(date_str, '%Y年%m月%d日')
    # 2023/8/5 形式
    elif "/" in date_str:
        return datetime.strptime(date_str, '%Y/%m/%d')
    else:
        raise ValueError("Unsupported date format.")

関数定義は、引数が str 型だとか返り値が datetime 型だとか typing まで活用してくれていて、素晴らしいコーディングです。

面白いことに、新しい ChatGPT セッションを開いて同じ要求をすると、その度に違った形のコードを書いてくれます。このブログを書く前に試したときの結果は、こんなのでした。(こちらには、typing がないですね。)

from datetime import datetime

def convert_to_date(date_string):
    # Try different date formats
    for format in ['%Y/%m/%d', '%Y-%m-%d', '%Y年%m月%d日']:
        try:
            return datetime.strptime(date_string, format).date()
        except ValueError:
            pass

    # If no format matches, raise an exception
    raise ValueError('Invalid date format. Please use one of the following formats: YYYY/MM/DD, YYYY-MM-DD, or YYYY年MM月DD日')

前者のコードは、私でもそう書くかな、という解法ですが、後者はちょっと想像できませんでした。そういう意味でも ChatGPT は面白いと思います。自分の普段の発想とは異なる設計手法に触れるというのは、勉強になるものです。

さて、コードは書いてくれましたが、ChatGPT 自身にテストして欲しいですよね。

: コードのテストもしてください。

すると、早速 Code Interpreter を動員して、ChatGPT 自身が書いた Python コードをテストしてくれます。(今回は、前者の関数 str_to_datetime()を得られたセッションで継続しています。)

この記事の末尾にあるリンクを参照して頂きたいのですが、ChatGPT の人間臭いポカも見られます。せっかくテストコードを書いたのに、元の関数 str_to_datetime() の定義を忘れて、エラーになってしまうのです。

しかしその後、無事に次のようなコードと回答を得られました。

# コードのテストを行います

def test_str_to_datetime():
    # 2023年8月5日 形式のテスト
    assert str_to_datetime("2023年8月5日") == datetime(2023, 8, 5)
    
    # 2023/8/5 形式のテスト
    assert str_to_datetime("2023/8/5") == datetime(2023, 8, 5)
    
    print("All tests passed!")

test_str_to_datetime()

ChatGPT: テストが成功しました。関数 str_to_datetime は、指定されたテキスト形式の日付を正確にPythonの datetime 型に変換できることが確認されました。

うーん、ValueError になるほうのテストもして欲しいですね。

: ValueError になるほうのテストもしてください。

def test_str_to_datetime_errors():
    try:
        # サポートされていない日付形式でテスト
        str_to_datetime("2023.08.05")
    except ValueError as e:
        print(f"Caught expected error: {e}")

test_str_to_datetime_errors()

うまく行きました。

ChatGPT に単体テストを書いて貰おう

ここまでの内容でも、人手で面倒な関数テストまでしてくれて十分に嬉しいのですが、今回のお題である、単体テストのコードも書いて貰いましょう。うまくいくでしょうか?

私: ちなみに、その関数 `str_to_datetime`を、あとで担当者が書き換えるかも知れません。regressionを防ぐために、Pytest のテストベンチを書きたいです。できるだけいじわるなテストで、20個のテストケースを含む、Pytest のテストコードを提示してください。

要した時間は 56秒(2023年8月7日現在)、こんなコードを書いてくれました。

import pytest
from datetime import datetime

# str_to_datetime 関数の定義は既に与えられていると仮定します。

# 正常な入力値と期待される出力値のペア
valid_test_cases = [
    ("2023年8月5日", datetime(2023, 8, 5)),
    ("2023/8/5", datetime(2023, 8, 5)),
    ("2000年1月1日", datetime(2000, 1, 1)),
    ("1999/12/31", datetime(1999, 12, 31)),
    ("2030年2月29日", datetime(2030, 2, 29)),  # 閏年のテスト
    ("2023/12/31", datetime(2023, 12, 31)),
    ("2023年1月31日", datetime(2023, 1, 31)),
    ("2023/2/28", datetime(2023, 2, 28)),
    ("2023年4月30日", datetime(2023, 4, 30)),
    ("2023/6/30", datetime(2023, 6, 30))
]

# エラーが期待される入力値
invalid_test_cases = [
    "2023.08.05", 
    "08/05/2023", 
    "2023年2月30日", 
    "2023/4/31", 
    "2023年13月1日", 
    "2023/0/1", 
    "2023年0月1日", 
    "2023/12/0", 
    "2023年2月0日", 
    "abcd"
]

@pytest.mark.parametrize("input_value, expected_output", valid_test_cases)
def test_valid_str_to_datetime(input_value, expected_output):
    assert str_to_datetime(input_value) == expected_output

@pytest.mark.parametrize("input_value", invalid_test_cases)
def test_invalid_str_to_datetime(input_value):
    with pytest.raises(ValueError):
        str_to_datetime(input_value)

せっかくなので、実際に pytest を走らせて貰いましょう。

: 実際にテストを走らせて貰えますか?

残念ながら、次のような回答が返ってきました。現在のところ、Code Interpreter は pytest ライブラリを持っていないようです。

ChatGPT: 申し訳ございませんが、私の現在の環境では直接 pytest を実行することはできません。しかしながら、提供したテストコードは標準的な pytest 形式に基づいていますので、ご自身のローカル環境やCI/CDパイプラインで問題なく実行することができるはずです。

実行方法は、テストコードを test_datetime_conversion.py などの名前のファイルに保存し、次のコマンドを実行することで確認できます:

pytest test_datetime_conversion.py

(略)

やってみることにしましょう…。あらら?

test_datetime_conversion.py:13: in 
    ("2030年2月29日", datetime(2030, 2, 29)),  # 閏年のテスト
E   ValueError: day is out of range for month

ダメじゃないですか。良く見ると、2030年2月29日を valid_test_cases に含めているのですが、2030年は閏年ではないですよね。

: やってみましたが、2030年2月29日で、ValueErrorになってしまいました。2030年は閏年ではないので、テストケースが誤っていると思います。

ChatGPT: 申し訳ございません。おっしゃる通り、2030年は閏年ではありません。このテストケースは不適切でした。

正確なテストケースを提供するために、正常な入力値と期待される出力値のペアを修正します。閏年のテストのために、代わりに2024年(実際の閏年)を使用します。

以下が修正後のテストケースの一部です:

こんなコードが出てきました。

# 正常な入力値と期待される出力値のペア
valid_test_cases = [
    # ... (他のテストケースは前と同じ)
    ("2024年2月29日", datetime(2024, 2, 29)),  # 閏年のテスト
    # ...
]

修正したところ、無事にテストはパスしました。

 $ pytest test_datetime_conversion.py
============================= test session starts =============================
platform darwin -- Python 3.9.11, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/yokoyama/tmp
collected 20 items                                                            

test_datetime_conversion.py ....................                            [100%]

============================= 20 passed in 0.04s ==============================

ちょっとやり取りが煩雑になってしまいましたが、ま、許容範囲でしょう。Code Interpreter はベータバージョンですので、将来の成長に期待しましょう。最後に、完全な test_datetime_conversion.py を示して貰いましょう。

: うまくいきました。できたら、最終版の test_datetime_conversion.py を示してください。

ChatGPT: もちろんです。以下が修正後の完全なテストコード test_datetime_conversion.py です。

(略)

詳しいセッションの内容は、こちらのリンクから御覧ください。

[加筆] もしかして、docstring で関数仕様も書いてくれたりしないかな?

ここまで記事を書いていて、ふと思ったのですが、関数仕様も書いてくれたりしませんかね。ユニットテストと同様、関数仕様をきちんとコメントで書くのも億劫で、後回しにされがちですよね。(← ファームロジックスの横山は、どれだけ怠け者なのか?)

関数を実装して時間が経ってから仕様を書くと、関数の実装と乖離してしまって、実装と仕様と、どっちが正しいの? なんてことになりかねません。よし、これも ChatGPT に頼んでしまいましょう。(← 「普通、実装を書く前に仕様書を書きませんか?」と言ったあなた。あなたが正しいです。)

: 最後に、関数 `str_to_datetime` の定義に、docstrings 形式で仕様を書き加えて貰えますか? 設計グループには日本人でない人もいるので、英語でお願いします。ついでに、他のコメントも英語に書き換えてください。

こんなコード(↓)になりました。素晴らしすぎます。こんな部下がいたら、管理者の皆さん、毎日 5時半に退社できるかもですよ!

from datetime import datetime

def str_to_datetime(date_str: str) -> datetime:
    """
    Convert a string representation of a date into a datetime object.

    Args:
    - date_str (str): String representation of a date in either "YYYY年MM月DD日" or "YYYY/MM/DD" format.

    Returns:
    - datetime: A datetime object representing the date.

    Raises:
    - ValueError: If the date_str is not in one of the supported formats or represents an invalid date.
    """
    
    # Check for "YYYY年MM月DD日" format
    if "年" in date_str and "月" in date_str and "日" in date_str:
        return datetime.strptime(date_str, '%Y年%m月%d日')
    # Check for "YYYY/MM/DD" format
    elif "/" in date_str:
        return datetime.strptime(date_str, '%Y/%m/%d')
    else:
        raise ValueError("Unsupported date format.")

細かいことを言うと、1行の長さが長すぎかなあ。私は Black を使うときも、--line-length 79 する派です。

: できたら、ソースファイルの1行は79文字以下で。

こちらのリンクを見ると面白いのですが、最初は失敗してます。Black みたいな整形ツールにライブラリ経由で渡そうとしたのでしょうか。ただし、最終的にはうまく書き換えてくれました。

今後も、オープンソースと AI を活用しつつ、ソフトウェア設計技術者の生産性向上を、ファームロジックスがお手伝いしていきたいと考えております。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。