前回、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様のお陰でバグが直ったよ~。元のプログラムがダメダメだったか。。。