
前回、Gemini様にPythonのスクリプトにpydoc形式のドキュメンテーション・コメントを挿入していただきました。「動作、よく分かっているみたいじゃん、だったらついでにユニットテストも書いてよ」ということで今回はプロンプトにお願い。時々ポカもあるものの、ほぼOKっす。お陰で年寄がバグ作っていたことも発覚。
※Google様の生成AI、Gemini(無料プランだけれども)を使ってプログラムを「吟味」してもらっています。
unittestとプロンプトへの入力
ユニットテストは、ソフトウエアの部品レベルの単体テストです。Pythonの場合、その名もズバリ unittest という名のモジュールがあるので、年寄はこれを利用させていただいております。
まあね、「テスト・ドリブン」というと先にテスト書いておいて、後から実際の機能をコーディングするみたいですが、忘却力の年寄は中途半端。多くの場合、関数の原型を書き、そのユニットテストを書き、テスト動かしてみて修正といったグズグズな感じで書いてます。今回のは、前回コメントを挿入した「動いている筈」かつ「とりあえずのユニットテストも含んでいる」ソースに対して、その中の関数を一つ取り出し、Gemini様にunittestの生成をお願いしてみようと。
プロンプトへの入力は以下です。
unittestモジュールを使って以下の関数をユニットテストする関数を作製して。
そして上記につづいて以下の関数をアップロードしてみました。なお関数のドキュメンテーションコメントは前回Gemini様に生成していただいたものです。
def freqIN(arg, dval):
"""Processes the frequency argument and converts it to Hz with unit prefix handling.
Args:
arg: The argument string containing the frequency value.
dval: The default value to use if the argument is invalid.
Returns:
The frequency value in Hz.
"""
prefixes = 'mkMG'
vl, lastChar = chkVal(arg, dval)
pfx = prefixes.find(lastChar)
if pfx == 0:
ex = 1e-3
elif pfx == 1:
ex = 1e3
elif pfx == 2:
ex = 1e6
elif pfx == 3:
ex = 1e9
else:
ex = 1
return vl * ex
Gemini様のご回答とそれへの対処
何時ものようにGemini様には三案回答いただきました。まずは第1案の冒頭部分が以下に。
上記につづいてunittestのテスト関数も生成されております。こんな感じ。
青のマーカは余計な部分、黄色は無効ではなく有効デス。どうも案1のテスト関数は「雰囲気でてるけど、コマケー誤解もありーの」という感じ。
実際、回答をファイルにして走らせてみると以下のエラーもあり。
この原因は、テスト対象の関数 freqINの内部で呼び出している chkValという関数がソースに含まれていないためであります。Gemini様はさらっと「完結したっぽい」したソースを生成していただいてますが、ターゲットの関数がちゃんと走るかどうかは、オメーの責任じゃろ、ということか。これはGemini様でなく私の問題ね。
慌てて freqIN の内部で呼び出している関数とその先の関数をとりこみました。そこでようやく、先ほどの見えていたアサーションの「誤解」問題に到達。以下のごとし。
アサーション部分に関しては案2の方が「当方の意図にあっている」感じがしたので、案2の方に切り替えることにいたしました。案2のテストが以下です。
案2も完璧ではなく、赤線引いた1000は不正ではなくOKな値です。100は正常な方のテストに入っているのに1000は不正とは首尾一貫しないGemini様です(よくあることか。)
案2の赤線抜きに差し替えて走らせてみたものの、またもやエラーが。
これは引数に空文字列与えたときのエラーみたい。これは実際のバグじゃないだろうか。。。
まあこの先はGemini様の問題ではなく、Gemini様に摘発された老人のコードのバグっす。テキトーにバグ修正を行い動作するようになった様子が以下に。
念のため、Gemini様生成のテスト関数と、それへの対処の手修正を反映したソースが以下に。
import unittest
def parse2float(st):
"""Parses a string to a float.
Args:
st: The string to be parsed.
Returns:
The parsed float value, or None if parsing fails.
"""
try:
work = float(st)
except:
return None
return work
def chkVal(arg, dval):
"""Checks the validity of the argument and converts it to a float.
Args:
arg: The argument string to be checked.
dval: The default value to use if the argument is invalid.
Returns:
A tuple containing the parsed float value and the last character of the argument string.
"""
vl = parse2float(arg)
if vl is None:
nLen = len(arg)
if nLen > 1:
bdy = arg[0:nLen - 1]
lastChar = arg[nLen - 1]
vl = parse2float(bdy)
else:
lastChar = "0"
if vl is None:
vl = dval
else:
lastChar = "0"
return (vl, lastChar)
def freqIN(arg, dval):
"""Processes the frequency argument and converts it to Hz with unit prefix handling.
Args:
arg: The argument string containing the frequency value.
dval: The default value to use if the argument is invalid.
Returns:
The frequency value in Hz.
"""
prefixes = 'mkMG0'
vl, lastChar = chkVal(arg, dval)
pfx = prefixes.find(lastChar)
if pfx == 0:
ex = 1e-3
elif pfx == 1:
ex = 1e3
elif pfx == 2:
ex = 1e6
elif pfx == 3:
ex = 1e9
elif pfx == 4:
ex = 1
else:
ex = 1
vl = dval
return vl * ex
class TestFreqIN(unittest.TestCase):
def test_valid_input(self):
# 正常な入力値に対するテスト
# 100Hz
self.assertEqual(freqIN("100", 0), 100.0)
# 10kHz
self.assertEqual(freqIN("10k", 0), 10000.0)
# 1MHz
self.assertEqual(freqIN("1M", 0), 1000000.0)
# 1GHz
self.assertEqual(freqIN("1G", 0), 1000000000.0)
def test_invalid_input(self):
# 不正な入力値に対するテスト
# 文字列
self.assertEqual(freqIN("abc", 0), 0.0)
# 空白
self.assertEqual(freqIN("", 0), 0.0)
# 不明な単位
self.assertEqual(freqIN("100x", 0), 0.0)
def test_default_value(self):
# デフォルト値のテスト
self.assertEqual(freqIN("abc", 100), 100.0)
self.assertEqual(freqIN("", 100), 100.0)
if __name__ == "__main__":
unittest.main()
やあ、Gemini様のお陰でバグが直ったよ~。元のプログラムがダメダメだったか。。。