若手エンジニアを不幸にしないための開発の「べからず」集が
長くなりすぎたので、テストに関する部分を別項目として独立させました。
まだ、随時、修正です。
以下の記事と内容がかぶっている部分があります。
もぐらたたき開発を卒業しよう
もぐらたたき開発を卒業しよう 対策編
##テストの「べからず」##
###[Test First]を実行しない。
- そんなわけで、コーディングの項目よりも先にもってきて見ました。テスト駆動開発TDDをぜひ試してみてください。『テスト駆動開発による組み込みプログラミング――C言語とオブジェクト指向で学ぶアジャイルな設計』
最初にこの本を紹介したように、テスト駆動開発を強く推奨します(まだ私はテスト駆動開発を使いこなせていませんが)。テスト可能な枠組みをソフトウェアの設計に入れることが必要なので、ソフトウェアの設計に強く関わっている手法だと理解しています。
###「テスト軽視」アンチパターン
間違った発言:「製品のコードとして残らないコードを書くことは無駄だ。その時間があれば、未実装の関数を書くことに専念しなさい。」
引き起こす結果:
- テストを自動化しない。
このような状況はとても不幸なことです。ソースコードの品質を確保することができません。ある時点で動いているように見えていたコードが、適切な動作をしていないことが発覚します。テストを自動化していない状況では、それが適切に動いていたのは確かだったのか、問題があったのに気づかずにいただけなのかの区別がつきません。
間違った発言:
「コードを修正したら、手動で一連の動作を確認してからコミットしてください。」
引き起こす結果:
手動で一連の動作を確認するには、かかる時間が非常に多いことを理解していません。またそのときの動作では、テストしきれない組み合わせが出ることも理解していません。
特に影響が大きいのはチームのリーダー以上の層がそのような意識を持っているときです。- 目視テストだけでテストすることで、テスト漏れを多数生じる原因を作る。
- テスト後のコードの修正をしたのに、今までテストでOKした部分を再チェックせずに先に進める。
- このことは、いつのまにか、ソースコードが劣化していることに気づかないままにしてしまうことになる。
テスト軽視は、不十分な品質のルーチンを使いながらその他のルーチンの開発を強要させることになります。下位層に位置する部分のテストが不十分なままに、上位層の開発をしていると何をしいているのかがわからない状況になってしまいます。
###テスト可能な枠組みを規定しない。
まちがった発言:「今は仕様のことを議論しているんだから、『その関数のインタフェースでは、テストしようがない』などとテストのことを今言うのは違うと思う」
引き起こす結果:
そのため外部に開発委託した内容に、どういうテストを行い性能を確保する必要があるのかを記述しないものになってしまう。
テスト不能なインタフェースになっているから、精度上の不具合がないかどうかを再現性のあるやり方でテストすることができない。そのため、「明確なバグ」はないという扱いになってしまう。
このようにときとして、ウォーターフォールモデルでの開発のときに、どのようにテストするのかを念頭においていないことがある。どのように自動化された単体テストするのかを経験してから、設計を経験することをお勧めする。アジャイルな開発スタイルは、設計・テスト・実装の全体について1人(もしくは数人のチーム)で経験することになるので、テストしやすい設計はどのようなものであるのかが分かってくるようになる。ウォーターフォールモデルで出戻りを生じない設計をするには、アジャイルな開発を経験してテストしやすい設計をすることが有効なのではないかと思う。
- その結果が、「単体テストされていないモジュールを受け入れて結合テストをする。」ことにつながる。
- テスト可能な情報が隠蔽されてしまっている設計なので、その外側で単体テストをすることが不可能になる。
- 例:カメラ入力やGUI入力をインタフェースとした関数しか実装せず、ファイルインタフェースの関数を単一責任の原理を満たしつつ実装しないので、再帰テストが不可能になる。
- テストしやすい枠組みを考えないので、ライブラリの依存性が相互に激しくなってしまって、保守性が低下する設計と実装のままになってしまう。
- テストされる対象のコードが単一責務の原理を満たしていないので、テストが何について完了していて、何について完了していないのかが分かりにくくなる。テストされる対象のコードが単一責務の原理を満たしていないので、書かなければならないテストの数が増えすぎてしまう。
###テストを自動化しない
テストを自動化しないことは、「最新のコードで***のテストは出来てるの?」に対して、「先週の時点ではできています」というのがやっとのレベルになってしまいます。どんなにテストを自動化しにく開発内容であっても、自動化できる部分は絶対あるはずです。そのような部分を切り出していって、自動化できる単体テストを1つでもいいから増やしていきましょう。
###誤動作の頻度が問題になる要因に対して手動テストを繰り返す
誤動作を生じる頻度が十分に抑えられているかをテストしなければならない要因に対して、1回の目視テストでOKとしてしまう。そのことは、100回に1度の頻度でしか発生しないことに対して、テストをしていないということを意味する。そのような状況では、出荷後に問題を引き起こすことになります。
###目視だけで証拠となる記録を残さない
間違った発言:「製品にはログを出力することなんてないんだから、テストのためにログを出力させるのは無駄だ。目視で確認すれば十分だ。」
本当にテストがうまくいったのかを第三者が納得のいく記録を残さないのは、テストして問題がある。証拠が残るようなプログラムの作りにしよう。うまくいった結果だと思っていたのが、実は問題を含んでいたかもしれないとなって、そのときのバージョンではどうだったのだろうということがある。
###トレーサビリティを考慮しない
間違った発言:「リリースした版でちゃんと動いていればいいんだよ。開発中のどの版の結果かなんていちいち動作ログに含めるなんて面倒なことなんてしなくていいから。」
その結果は、どのバージョンのプログラムの結果なのですか、どのバージョンの設定ファイルとの組み合わせなのですか。実行結果に対してトレーサビリティを考慮していないことは、動作状況の再現性を損なう。また、問題を生じたときの要因の解析をするためにもトレーサビリティは重要な要因だ。そのような仕組みをプログラムのソースコード、使用するデータのデータ形式などに入れるように工夫をすることだ。
###一連動作の中でしかテストできないことは、開発の速度を遅くする
間違った発言:
「とにかく実機で結合試験で動かしなさい。過去のログデータの再処理なんてどうでもいいよ。」
もたらす結果:
一連の動作を新たに繰り返せる回数に限りがあるので、過去ログデータの再処理ほど回数をこなせない。
また、そのつど新しい状況でテストするので、前にトラブルを生じたデータの状況で今のアルゴリズムの性能がどうなっているのかを知ることができない。
そのような理由から、モジュールでのテストと結合テストの両方を行うことが重要と考えます。
###コーディングのバグとアルゴリズムの限界とを区別しない
コーディングのバグではなく、アルゴリズムの限界である現象についてはテスト・デバッグがしにくい。
###複合的な要因があるテスト項目に対して、1つの要因についてしか着目せず、問題のありかを見誤る。
このような状況はとても痛い結果となります。問題がある可能性が高いとして疑われた部分を担当している人は、原因不明のバグの原因が、自分の担当したコードではない部分にあったときに、ほっとすると同時に、他の要因についても考えてくれれば、こんなに苦しまなかったのにと思うことになります。
###テストに値するコードに改善しないまま手動テストを繰り返す。
手動テストはせっかく実施したのに、コードを少し改変したら、今のコードでその手動テストが問題なく動作するのかどうかは「まったくわからない」ものになってしまいます。せっかく手間をかけたテストの労力が無駄になってしまいます。
ですから、上に述べたようにテストを自動化しましょう。
やむなく、手動テストする場合には、その手動テストが無題ならないようにしておきましょう。
たとえば、次のような要因があるとせっかくの手動テストにかけた労力が無駄になりかねません。
- #ifdefのコードはテストを厄介にする。
テストされる対象のコードに山ほどヘッダファイルで#includeされて、#defineされるマクロ定数があると、テストされる対象のコードはそれらのマクロの影響を受けてしまう。マクロ定数が書き換えられてしまうと、テストされる対象のコードは正しく動くコードであるのかどうかがわからなくなってしまう。このことについては以前読んだ本の中にも書いてある*「条件コンパイルはほとんどテスト不能なのだ。#ifdef は、1つのプログラムを2種類の別々にコンパイルされたプログラムに変身させる。そうしたすべてのバリエーションがコンパイルされてテストされているとは断言できない。」*(Kerninghan, 福崎訳 『プログラミング作法』p.274)
- テストをすることによって、自作ライブラリのある範囲について設計が適切であって、仕様どおりの動作をしていることを保証したいわけです。それがマクロ定数によって影響を受けてしまうと、そのライブラリの保証が不明確になってしまうのです。
###異常系の処理が適切に行われているのかのテストを工夫しない
異常系の挙動は、細工をした状況にしないと、テストができない場合があります。あえて間違ったパラメータを与えて通常の処理がうまくいかないようにするなどの細工を行わないと、異常系のテストの前提条件に達しないことがあります。これを、なにも工夫せずに、異常系の処理が呼ばれるのを待っていると、相当の時間をもってしても、異常系のテストができなくなります。
###要因の切り分けができない結合テストをする
結合試験を手動テストで実施したときに、何かがうまくいかないときに、その原因がどのモジュールにあるのかを切り分けることができない内容で結合テストをしてしまう。
そのため、何かがうまくいかなかったことは分かるが、どの要因でうまくいかなかったのかが分からないままになってしまうので、原因の解析は簡単ではない。
-
失敗する要因を事前に分析しない。
- 複数の失敗する要因があるときに、その切り分けをできないまま、結合試験をしてしまうこと。
- 単体テストと違って、組み合わせの数が増えてしまう。そのために要因の切り分けに必要な時間は結果的にふえてしまいやすい。
-
テストする項目のチェックリストを作らない。
- このような状況に対して、"もぐらたたき開発"という造語をつくってみました。もぐらたたき開発を卒業しよう
-
そのコードがどのように動作すべきであるのかを規定している文書が存在していないようなモジュールを受け入れる。
- 関数のヘッダファイルの中に規定してるのがベストだが、それ以外の参照すべき文書のファイル名くらいは、ソースコード中に書かれていて欲しい。十分に知らされていない挙動を、「それはそういうもんだと思ってくれ」と後から言われても、それはテストを否定するものである。
- ソフトウェアのテストが遅れている理由にある、別工程で生じている理由を無視して、「とにかくソフトウェアが遅れている」と別工程にある問題への対策を講じないこと。
- しかも悪いことに、その「別工程で生じている理由」は何度でも再発する。
-
テストのための手順をドキュメントで共有しない。
- ある時点で書かれても、バージョン管理しやすいファイル形式ではないもので書かれていると変更点が見つけにくい。
- そのため、間違った手順でテストし、間違ったバグレポートを作ってしまう。
- 当面の目的には十分な範囲でテストせず、過大な要求をしてうまくいかないと問題にしてしまう。(眼帯とマスクとを同時にしている人の顔が検出できなくたって、顔検出を責めないでね。)
-
問題点を早めに発覚させることが開発を幸福にさせることだと考えない。
- 問題点が見つかることを厄介ないやなことと考えないのがいい。むしろ、問題点が明確になったことで、何を対処すればよいのかが明確になったともいえる。物理学の歴史でも、既存の理論が破綻する実験事実が出てきたときが、物理学の進歩につながっていることを示しています。ですから、あなたの抱えている開発の中で問題点が見つかることを悪いことと考えないようにしていきましょう。 -
テストを誰にでもできる簡単なことと思い込む。
- テストの作業のために時間を費やしてくれている方々に、技術者としてのノウハウを共有しないままになってしまって、コーディングや設計の分野の力を十分につけさせることがないままになってしまう。
- 新規に実装した部分のコードをテストせずに、ビッグバンテストをする。
- 単体テストを済んでいるモジュールを、既存のシステムに結合する部分に対してテストを書かずに、全体のビッグバンテストをして、デバッグをしてしまう。そうすると、新たなバグを埋め込んでいる可能性が残ってしまう。
-
動作検証をコードを書いた人だけに担当させ、コードのバグの発見と対策とを兼務させてしまう。
- 潜在的なバグをなくすためのコードの見直しがその分損なわれてしまう。
- 現象として発覚したバグ対策だけ行い、同じような理由で生じうる顕在化していないバグ対策を実施しない。
-
開発で品質が確保できなかったときの悪影響に対する恐れをもたない。
- 納得のいく品質を得られていることを誰もが納得できるようにするにはどうしたらよいのかを考えないこと。
- 少ない数のチェックでは品質が確保しきれないことが、数値計算の分野では多いということを考えないこと。
- ノイズのあるデータが入ったときに、どれくらいアルゴリズムが耐えられるかを評価しないこと。
-
バグは常に新規改変のコードの中にあると100%信じきること。
- 時としてバグは、変更分にではなく既存コードの部分にあって、新規改変によってバグが発覚することがある。
-
バグはその中で中で使われている関数にあるとだけ信じて、使っている側を疑わない。
- 使われている関数側のバグを疑うことは多いが、使っている関数側のバグも疑う必要がある。両者の関数インタフェースに対する理解が不整合を生じているとバグになる。
###網羅的なテストができていない巨大なソフトウェアは、信頼のできるプログラムになることはない。
組織としてのプログラムの開発が素人の開発になっていると、個々のモジュールを作っているソフトウェアエンジニアの一部が単体テストを重ねていても、全体としての出来は、素人のソフトウェアになってしまう。設計の弱い部分が足を引っ張ると、他の部分が動作していても、信頼のできるプログラムにはならない。網羅的なテストができていない巨大なソフトウェアは、いっけんコードが目的の作業をするように書かれきっているように見えても、信頼のできるプログラムにはなっていない。網羅的なテストができるようにするためには、ソフトウェアを大幅に書き直していかなくてはならないので、実は完成への道は遠い。
-
そのテストで何のフィードバックを開発に返そうとするのかを意識していないテストを立案し推進する。
テストは、開発にフィードバックする情報をあげて、それによって開発の役にたたねばなりません。しかしながら、そうなっていない事例もまれに見ることがあります。開発のフェーズにあわせてテストする内容を考えなければなりません。「そんなこと開発の現場ではわかっているよ。」などというバグレポートをあげるだけのようなテストの立案・推進をしてはいけません。何人月の作業量を委託したのですから、それに見合う成果物を得て、開発加速しなければなりません。- 開発の初期は、ブラックボックステストよりも、ホワイトボックステストで、モジュールの設計と実装をテストするのがお勧め
- 開発の中で「設計と実装が枯れてきた」と信じる部分からテストするのがよいと思う。
- 複数の機能を同時にテストするのではなく、1つの機能に絞り込んだテストの方が問題点を整理しやすい。
- 動作ログを残して再現テストができないようなコードでは、手動でのテストが無意味になる。
-
デバッグ作業の中で単体テストを書かない。
単体テストの形にしておけば、テストがいつでもできるので、バグの再発を予防できる資産となる。
-
問題の切り分けをできるようにするための工夫を予め準備しておかない。
問題が発生したときに、問題をどうを切り分けるかが、デバッグ作業を簡単にする。
問題が特定の1台か、他のマシンでも発生するか?
問題が発生したのは、いつのバージョンから突き止める。
そういった、問題が発生してからのアプローチもあるが、予め問題が発生しそうな部分に対して、予防的なプログラミングをしておくことが必要だ。ファイルを指定されたときに、ファイルがなかったり、ファイルが空だったり、ファイルが壊れていたりすることがある。そのようなときには、それに応じた対策をコーディングしておかないと仕事としてのソフトウェアにならない。
トラブルが発生しうるものに対しては、予め発生するものとしてコーディングをしていくことだ。
問題が発生しても無視して次に進むよりは、開発時点では、assertでチェックしておく。
そういう当たり前のことをどれだけきちんと積み重ねておくが、仕事のソフトウェアでは必要だ。 -
完成度が低い時点で、ブラックボックステストを高次の階層で実施する。
ソフトウェアの完成度が低く、個々のモジュールで用いているアルゴリズムの性能がでていないことを、開発者自身が熟知している時点では、ブラックボックステストを高次の階層で実施することにあまり意味は見出せません。しかもそれを自動化しないテストで行っている場合には、開発にフィードバックできる情報は、極めて限られます。
完成度の低い時点では、ホワイトボックステストを低次の階層で行うことの方が有意義と思われます。
テスト項目は、アルゴリズムの判定精度に依存しない項目と、判定精度に依存する項目とに分けて管理すべきと考えます。顔画像で年齢を推定して、販売の可否を判定する自動販売機があったとします。「20歳以上と判定したら、販売を許可する。」、「20歳未満と判断したら、販売を許可しない。」の動作をしているかどうかの項目のテストがあります。他に、「この顔を20歳以上と判定するか」というテストとがあります。前者のテストの場合、満たさなければバグと言い切れます。後者の場合、バグなのか性能限界なのががはっきりしないものです。これを漠然と、「この顔を与えたときに、販売を許可すること」とすると、問題の所在を見極めきれない評価になります。 -
Visual StudioのDebugモードとReleaseモードでは挙動が違う。
C++のプログラムをVisual Studioを動作させると、DebugモードとReleaseモードでは挙動が違うことがある。特に変数のスコープによる寿命の関係が影響しやすいようだ。DebugモードではReleaseモードのときよりも変数の寿命が延びるようで、Releaseモードでだったらバグになる状況ソースコードが、Debugモードでは問題が発覚しないことがあった。動的に変数の領域を確保していて、それをfreeするタイミングが影響していることがあった。
なお、そのような状況に陥ることを減らすために、ポインタ渡しではなく、参照渡しを使うように心がけるようになった。IplImage型は使わずに、cv::Mat型を使うようになった。malloc(), free()は使わないようになった。fscanfは使わずに、yamlファイルなどのファイル形式を使うようになった。
- コンピュータの中で閉じないシステムの場合には、コンピュータの中で閉じるシステムとは別なテスト方法になることを理解しない。
- 画像入力のシステムは、想定している見え方の画像は入ってこないという可能性がある。
- 通信系は、通信系のトラブルを生じる可能性がある。
- 人が入力するデータは、入力データに間違いがあるという可能性がある。
- 作成したロットごとに特性の違いがある場合には、そのロットごとの違いが、システムで期待する性能に影響を及ぼすかもしれない。
- キュウリの自動判別のシステムがあるとしよう。この場合、コンピュータ(例:RaspberryPi 3)だけではシステムが構成されていない。camera入力があるし、キュウリを判定結果に応じて別の箱に入れる工程がある。モーターが期待どおりの動作をしないというトラブルがあるかもしれない。そういったときに、キュウリの画像による識別だけをファイル入力ベースでテストしても、目的のキュウリの仕分けで楽をすることにはつながらない。失敗しうる全ての要素(もしくは主だったもの)についてテストをする必要がある。
###すべきこと
- 単体テストの積み重ねとしてアプリケーションを作りあげる。
- 単体テストのカバレージを管理して、一定以上のカバレージになるように開発を進める。
- ヘッダファイルの記述とドキュメンテーションコメントでも残る仕様の曖昧さは、単体テストの期待値動作によって、関数の曖昧さが排除されて明確にする。
- 単体テストの重要性を開発チームのマネージャーに理解させること。
テストのしかたに問題がある場合には、開発の進め方に問題があるように思える。
どのように開発のどこまでが完了したのか、
開発のアプローチの解決していく項目の優先順序が適切であるのか見直してください。
特に機械学習系の場合には、100%完璧な結果がでることはないので、
それを前提とした開発順序をすることだと思う。
(機械学習については、別途書く必要があると思っています。)
-
ロジック上のバグと精度上のバグは区別しよう
機械学習では100%の精度がでることはありません。「顔画像から未成年と判断したときにはお酒を売らない」という部分は、機械学習の精度とは関係のない部分です。こういう部分にバグがあるかないかのテストは「ある顔を未成年と判断する」こととは無関係に判断できることです。まずは、ロジック上のバグを単体テストしながらつぶすことです。精度上のバグは、たいがいの場合とても手ごわくて、なかなか0にできません。 -
動作先のプログラム、設定ファイル、使用データのバージョンが整合性があるのかをチェックできる仕組みを予め仕込んでおく。そうしないと、間違った組み合わせで動作させられてしまって、そのトラブルシューティングをさせられてしまう。
デバッグ用のコードがメンテナンスを妨げてないか
組込みプログラミング > リッチでない環境でのデバッグ方法
ソフトウェアの品質についてはIPAやSESAMEなどが多数の資料を公開しています。
これらの情報を、社内の担当者に知らせることは、このqiitaのページを知らせるよりははるかに
効果的かと思います。
追記
継続的インテグレーション(デリバリー)サービスを利用しないという罪悪
テストの手法について、世の中には情報が満ちてきている。
組織の運営の中でテストについて問題を抱えていると思ったら
チームの人たちに、テスト駆動開発などテストを重視した開発現場の人たちと意見交換する場所を提供することだと思う。
しかも、テスト駆動開発とリファクタリングを重視しているチームの経験を伝えることだ。
コードの品質を保ちつつ、設計を改善していき、機能を追加していく、それを可能にしていく方法は、今どきのテスト手法を用いて、モジュールの品質を保って設計を改善していくことだ。
テストで品質が確保されている限り、設計の見直しを安心して実行できる。
とりわけテストを自動化していると、品質の確保をしやすい。
すべての項目についてテストをすることを考えるよりも、自分の開発している中でクリティカルな要素についてテストをすることだけ考えよう。
そして、それを自動化テストすること。
仕事がうまくまわっているならいい。しかし仕事がうまくなっていない状態なのに、その状態を脱するための努力を組織として実行できていないことが問題なのだ。
追記
明快なテストは、仕様を明らかにすることにつながる。
どうしても言葉だけでは伝わりにくい部分がある。
それをテストで自動化すると、そのモジュールの動作が明快になる。
正しい使い方はどうなのか、
異常が生じた時の動作はどうなるのかが、
テスト項目に入っていて自動化されていると、
そのモジュールの動作が明快になる。