要所に単体テストを配備しよう ー バグから学んだこと


データ解析の最終段階で、原稿用に数字を確認していく作業で、サンプル数 n の不一致から次々にバグが見つかりました。それらはどのファイル、どのデータが解析に含まれるかを左右するバグでした。

これらをひとつひとつ原因究明して修正する度に、データファイルを更新し、論文の図も、統計検定の結果もやり直しになりました。

数年を要して作り上げたカノニカル・パイプラインでは、自動化できない手作業の工程を最小限とするように作られており、Live Scriptの実行時に出現するダイアログボックスで、ファイルの同期・更新・再計算を選ぶと、

  1. データファイルの同期・更新を半自動的に行い(MATLABから直接Spike2のエクスポート機能を動かすことができないので、途中でSpike2を数クリック操作する必要性が残りました。エクスポートするファイルが指定して保存してあるテキストファイルを選ぶだけです。)、
  2. データを再計算し、
  3. 統計検定をやり直し、
  4. どうしてもIllustratorでしか出来ない仕上げ作業の工程を僅かに残して(具体的には、ハイフンをen-dashに置き換える、確認用に表示してあるサンプル数 n を不要なところについては除去する、パッチ・オブジェクトの三角形をマージしてひとつのパスに統合する、legendの棒の長さを短くする)、figureを自動的に作り直す、

という作業を自動で行います。

この最終段階で遭遇したバグのひとつ毎に、もしもこれらの作業を手動で行っていたとしたら致命的な大打撃でした。

特に上の工程でいうと、1番と4番の自動化がここでは非常に重要でした。このカノニカル・パイプラインをもし作っていなかったら。これを乗り越えるのは不可能だったんではないかとさえ思います。あまりにも開発に時間と労力がかかったので、ずっとこれの開発の必要性を自分でも疑問視していましたが、必要だったと初めて実感しました。(そんな怪しげなモンをこんな公なところで紹介するな?誰か他の人が、これは大事だと言ってくれたら大事だと分かるかなと期待していたのですよ、ホホホ)

1番のファイルの同期・更新の自動化部分は、Gitレポジトリのコミット履歴を見ると 2014-1-20の一番最初のコミットにも関連する関数がひとつ含まれており、2014-11-12から活発に開発を始めています。これがいつ頃「完成」したのか、….あまりにも人生に色々あり過ぎて忘れてしまいました。コミット履歴は全部残っていますが、どの時点で「できた!」と判断したのかもはや思い出せません。2016年の夏頃だったような気がすると思って調べると、6月にこの部分の自動化のテストコード(単体テスト unit test)を書いています。ということは、短く見れば一年半、長く見積もればほぼ三年かかっています。

あまりにも複雑になったので、動作するようになったものの、二度と触りたくないと思っていたら、最後の段階でのバグはやはりここに関するものが多く見つかりました。

以下は今回の経験から学んだことです。

1. データの格納してあるフォルダ・パス指定の誤り

対策としては、**データを格納するフォルダの数をできるだけ少なく、シンプルな設計にする。何十種類ものフォルダに細かく分類してデータを格納するのは避けるべきだと思います。

これは実際に数を少なくしてあったので、間違いこそ生じましたが、原因が見つかれば直すのは簡単でした。

2. 正規表現によるファイル名の解析の誤り

ファイル名には命名規則があり、それを正規表現で解析して、ファイルの同期・更新の際に利用していたのですが、私の正規表現の理解が単に不十分であったためにここに誤りが複数潜んでおり、命名規則の例外的ケースに相当する、一部のファイルが処理されずに漏れるという事態が発生していました。

正規表現が苦手なのは薄々知っていながら、これに対してテストコードを書いていなかったのが失敗でした。

命名規則の範囲で出現する、あらゆる例外的なパターンのファイル名について、代表例を準備しておき、正規表現がそれらの例外を正しく処理できるかどうかスクリプト・ベースの簡単な単体テストを書いておくべきでした。

そして、必要に応じて、新たに例外的なファイル名を認めざるを得なくなったとき、必ずその単体テストに新たなassertionを一行追加するように徹底することで、問題を回避できただろうと思います。

3. 巨大Excelワークシートの分析

今回は実際にはここに関するバグは見つかりませんでしたが、ファイルの同期・更新に関して、最も複雑なコードは、この巨大Excelワークシートから正確に指定された情報を取得する部分でした。

実験にまつわる現実的な問題のために、実験の仕様、そして実験データの仕様が頻繁に変更され、それを間違いなく処理するために、非常に複雑な if文による例外処理が必要となっていました。

この部分は明らかに開発上のネックになるのですが、今回の経験で対策がひらめきました。

matlab.unittest.fixtures を利用した、クラス・ベースの単体テストを書いて、それぞれの実験データの仕様について、テストデータとしてのExcelファイルを準備し、動作を検証するようにします。

新たに実験データの仕様が変更される度に、また新たに変数(列)が追加される度に、この単体テストに新たにテスト・ケースを追加することを徹底すれば、Excelワークシートが複雑化することによるバグの発生を防ぐことができるでしょう。

この単体テストは一度に作るのは大変なので、まだデータの形式が単純なうちから、同時に配備することが大切なように思います。テスト駆動開発の発想でExcelワークシートを、単体テストと共に成長させていけばよいのだろうと思いました。

...と、ここまで書いてから調べてみて気がついたのですが、実は実際にそれぞれのデータ仕様に対応したテストケースを持った、クラス・ベースの単体テストを私はちゃんと書いていたのでした(笑)。だいぶ前だったのですっかり忘れてた。道理で、このややこしいステップでバグが出なかったわけです。

データ解析のカノニカル・パイプライン

Live Scriptを使用。探索目的のスクリプトの命名法は、scr20170725_112030_hogehoge.mlxという形式。発表用のスクリプトは、区別できるようにmain1_hogehoge.mlx(Figure 1に対応)という形式。

  1. 環境設定
    • フォルダパス設定、パラメータなど
    • データを格納するフォルダはできるだけ数を減らし単純な構成を心掛ける
  2. ファイルの同期・更新
  3. データの解析計算
  4. 統計検定
  5. 図の作成

まとめ

あらゆる関数に対して単体テストを書くのはおそらく非効率だと思いますが、扱う処理の種類によっては、複雑さのために、動作の正しさが頭では予測しづらいことがあります。

正規表現などは典型で、こういう風に動くだろうと高を括っているとまったく結果が違っていたりするので(慣れの問題でしょうが)、単純な単体テストによってかなり間違いを減らせるでしょう。

複雑なクラスを作るときは、初めから単体テストと同時に設計を始める、テスト駆動開発を採用しています。テスト・コードがなければ怖くて作れないというのが正直なところです。

今回の失敗は、カノニカル・パイプラインの価値を実感したこと、正規表現の理解が深まったこと、単体テストのさらなる有用性が見えたことに意義がありました。