はじめに
今回は、テストについてです。
実装を進めていく中で、途中からかなり強く感じるようになったのが「テストはやはり必要だ」ということです。
理由は単純で、
- command が本当に通るか
- 表示が崩れていないか
- mock と real で大きくズレていないか
を、その都度自分で目視確認するのは限界があるからです。
最初はかなり素朴な考えで、
- まず pass / fail が分かればよい
- 必要なら特定文字が含まれているかを見る
くらいで十分だと思っていました。
ただ、実装が増えてくると、それだけでは足りなくなっていきました。
最初は shell test から始めた
最初に書いたテストは shell でした。
この段階では、
- command を呼ぶ
- return code を見る
- 特定の文字列が入っているか / いないかを見る
くらいの確認ができれば十分でした。
実際、list や show のような比較的単純な command をざっと見るだけなら、shell でもかなり役に立ちます。
この時点では、テストに求めていたのは
- とにかく動いているか
- 露骨に壊れていないか
を素早く確認することでした。
分岐が増えると shell ではつらくなった
ところが、実装が進むと shell ではだんだんつらくなってきました。
例えば、
- mock と real で分岐する
- 出力の確認項目が増える
- 条件ごとに期待する文字列が変わる
No Error Log in Nvmeのように出力が 2 通りあり得る
といったケースが増えてきたからです。
shell でも書けないわけではありません。
ただ、分岐が増えるほど、
- 読みにくい
- 修正しにくい
- 何を確認しているのか分かりにくい
という問題が出てきました。
このあたりで、「これは Python の方がいい」と感じるようになりました。
Python に移ったのは、観点が増えたから
Python に移った理由は、単に shell より好きだったからではありません。
一番大きかったのは、テスト観点が増えてきたことです。
Python だと、
- 分岐を書きやすい
- helper を作りやすい
- regex も扱いやすい
unittestのような test 用ライブラリが使える
ので、「何をどう確認したいか」をかなり表現しやすくなります。
特に今回のように、
- CLI
- mock / real
- 出力文字列
- 異常系
が混ざってくると、shell の 1 本の script より Python の方が整理しやすいと感じました。
それでも見逃しは起きた
ただ、Python に移ったからといって、それで十分だったわけではありません。
途中で何度か起きたのが、
- テストは全部 pass している
- でも AI review で見逃しを指摘される
というケースです。
特に分かりやすかったのが CLI です。
例えば新しく --count を追加した時、
errinfoでは使える- でも
showやidentifyやlistでは reject されるべき
という仕様になっていました。
ところが、実装では parser だけ直して、他 command 側の reject を入れ忘れていました。
テストが pass していても、そこを見ていなければ当然見逃します。
ここで初めて、「テストコードの量」よりも 何を見るかを決めることの方が重要 だと理解しました。
pass case だけではなく、fail case を分ける必要があった
そこで考えるようになったのが、異常系をどう分けるかです。
最初は単純に
- pass するか
- fail するか
だけで見ていました。
でも実際には、fail と言っても中身が違います。
例えば CLI なら、
- option 自体が無い
- option はあるが値が無い
- 値の形式が不正
- その command では使えない option
- 数値としては読めるが範囲外
- 対象 serial が存在しない
- duplicate option がある
のように、意味がかなり違います。
ここをまとめて「fail」としてしまうと、何を見ていないのかが分からなくなります。
そのため、途中からは
missinginvalidunsupportedout-of-rangenot-foundduplicate
のように、fail pattern を分けて考えるようにしました。
新しいものを追加するたびに、この観点でのテストパターンはないかを考えることで、ヌケモレが多少増しになりました。
観点を可視化するために matrix を作った
ただ、それでも見逃しは起きます。
今回のケースでは、前述のFail Patternは確認しても、もとのコマンドに対する確認ができていない、ということが起こりました。
そこで、チェック項目でmatrix を作りました。
今回作ったのは主に 2 種類で、
command × optionoption × fail case
です。
前者では、
listshowidentifysmarterrinfo
の各 command に対して、
serialsourcecount
が使えるかどうかを確認しました。
後者では、
serialsourcecount
それぞれについて、
missinginvalidout-of-rangeduplicate
などを少なくとも 1 回は見たか、を整理しました。
この matrix を README.md に残したことで、
- 何を確認済みか
- どこが未確認か
がかなり見えやすくなりました。
実際、--source の抜けや duplicate option の確認漏れも、この表を書いたことで見つけられました。
今回は lower layer までは踏み込まなかった
一方で、今回のテストはかなり CLI 寄りです。
つまり、
- parser
- command の挙動
- mock / real の出力
までは見ましたが、
openioctl- timeout
- retry
のような lower layer は、今回はほとんど見ていません。
ここはやりたくなかったというより、今の段階ではそこまで手を広げると整理しきれないと感じたからです。
なので v0 では、
- まず CLI と出力の観点を固める
- lower layer は v1 以降の課題とする
という割り切りをしています。
まとめ
今回テストを追加していく中で感じたのは、テストコードを書くことよりも、何を漏らさず確認するかを整理すること の方がずっと大きかったということです。
流れとしては、
- 最初は shell で pass / fail を見る
- 分岐が増えて Python へ移る
- それでも見逃しが出る
- fail pattern を分けて考える
- matrix で可視化する
という形でした。
結果として、今回一番効いたのは「Python に移ったこと」そのものより、
- 観点を分ける
- それを README に残す
- 抜け漏れを可視化する
という運用だった気がします。