ユニットテストで振り返るプログラマとしての自分史

ここ 1, 2 年、主に キューブ・ソフト で配布している Windows ソフトウェアを中心に、ユニットテスト周りの整備を精力的に行ってきました。今回は、テストと言う視点で、自らのプログラミングの軌跡を振り返るような記事を執筆してみようかと思います。

テスト以前

私のプログラマとしての経歴は 2001 年 4 月に大学の情報科学科に入学した事によって始まります。入学するまではプログラミングの経験もなく、本当にゼロからのスタートでした。入学からしばらくの間は、講義で出される課題を解いていく日々と言う感じで、触れたプログラミング言語こそ、Pascal、C、JavaCASL II、PerlStandard MLVHDL と多岐に渡りますが、実際には書き捨てのプログラムが大多数を占めていました。

この記事を書くにあたり、何か当時の成果が残っていないかバックアップ HDD を漁ったところ、ROVND と言うプロジェクトを発見しました。私は RAGNAROK Online と言う MMORPG の最初期に「ゲーム内(Loki から Heimdal サーバ)で取引されるアイテムの相場を調査して公開する」と言う事を行っていましたが、時が経ち、ゲーム内で流通するアイテムの種類が増えていくにつれて、手動による更新に限界を感じ始めていました。そこで、プログラムによる半自動化を試みたのが最初のプロジェクトとなります。内容的には、価格データ (price.txt) 等の内容を基に、静的な HTML を生成するものです*1。日付を見ると、2002 年~ 2003 年の終わり頃まで続けていたようです。

ROVND はテキスト処理を C のみで行うと言う、今考えると茨の道を選択していますが、困ってから根性で何とかすると言うスタンスで、最初の数年は過ぎていきました。この頃は、テストどころか保守と言う概念すらありません。バージョン管理と言う概念を知るのもまだまだ先の事です。

Edit and Pray からの脱却

プログラマとして最初の転換期となったのが 2004 年です。この年、初めて講義とは関係なく、自らの意思で C++ の学習を始めました。始めた理由は、何となくカッコイイからと言う程度のものだったのですが、学習し始めると同時に何故か「汎用的な C++ ライブラリ」を作り始めます。これが後に CLX C++ Libraries と言う名前となり、公開している中で最初の成果物となりました。記録を見ると初公開は 2006 年 2 月 22 日だそうなので、プログラマになって 6 年目と言う事になります。

CLX C++ Libraries は、私にとって思い出深いものとなりました*2。2008 年に OSDN(旧 SourceForge.JP)より 今月のプロジェクト として取り上げて頂いた時には、初めて自分のプログラムが注目されて嬉しかった事を覚えています。また、このプロジェクトのおかげか @cpp_akira さんに Boost.勉強会等に誘って頂き、日本における C++ のコミュニティについても知る事ができました。

プログラミングに関する基本的なスタンスや バージョン管理 への理解なども、このプロジェクトを通じて徐々に固まっていきました。しかし、自分以外の誰かが自分のプログラムを利用する と言う状況になり、遂に保守に関する問題が露呈し始めます。CLX C++ Libraries は、自分の思い付きでライブラリを追加・修正していましたが、少しずつ「公開されているサンプルファイルすら動かない」と言う報告を頂くようになりました。

ソフトウェア開発の失敗談

修正した部分に関しては手動による確認を行っていたのですが、「まさかそんな所に影響があるとは……」と思うようなファイルに対して、エラーが見つかる事が多々ありました。自分が完全に把握していると思っているプロジェクトですらこの有様なのだと、プログラミングに対する恐ろしさを教えてくれたプロジェクトでもあります。

この時点では、まだ「ユニットテスト」と言う概念は知りませんでしたが、何とかしなければと言う危機感はありました。しばらくの間、この問題に対して悩んだ結果、ライブラリと一緒に作成していたサンプルファイルを利用する事を思いつきます。CLX プロジェクトでは、新たなライブラリを追加すると、そのライブラリを利用するための簡単なサンプルコードも併せて作成する形で更新を続けており、この時点で 100 ファイル程度のサンプルファイルが手元に存在しました。そこで、これらのサンプルファイルを下記のようなシェルスクリプトで全て実行し、コンパイルエラーが存在しないかどうかを確認する事でテストとする方針にしました。

for file in `ls example/*.cpp`; do
    command="$CC $CFLAGS $file $LDFLAGS"
    $command 2>$TMPLOG >/dev/null
    if [ -e $TMPLOG -a -s $TMPLOG ]; then
        cat $TMPLOG >>$ERRLOG
    fi
done

対象とするコンパイラGCC、Visual C++、および Borland C++ でしたが、全てのコンパイラに対して毎回コンパイルを実行すると数 10 分程度の時間を要していたため、実際には、通常時は GCC のみでリリース前のみ全てのコンパイラで検査する形を採用していました。これにより、完全なテストとは言えないまでも単純なコンパイルエラーは検知できるようになり、何もしないよりはマシと言うレベルには改善する事ができました。

はじめてのユニットテスト

2009 年頃から、利用者数的には恐らく最大の成果物となる CubePDF の開発がスタートしました。私自身は、ちょうどこの前後でユニットテストと言う概念を知ったため、C# による開発では比較的早い段階から NUnit を利用したユニットテストを用意する事に成功しています。しかし、この時点では少なくとも 2 つの問題が存在しました。1 点目は見た目と処理の分離 (PDS: Presentation Domain Separation) と言う原則が理解できていなかったため View に近い部分のユニットテストが書けずにいた事、そして 2 点目はテスト時間です。

[Test]
public void TestConvertFileType() {
    foreach (Parameter.FileTypes type in Enum.GetValues(...)) {
        UserSetting setting = new UserSetting();
        setting.FileType = type;
        setting.PostProcess = Parameter.PostProcesses.None;
        ExecConvert(setting, "-type");
    }
}

private void ExecConvert(UserSetting setting, string suffix) {
    string output = System.Environment.CurrentDirectory + @"\\results";
    foreach (string file in Directory.GetFiles("examples", "*.ps")) {
        /* ... */
        bool status = File.Exists(setting.OutputPath);
        Assert.IsTrue(status, "File.Exists: " + file);
    }
}

上記は CubePDF における変換処理のユニットテストの一部です。これらのユニットテストを記述した当初は、事前に用意したいくつかの PostScript ファイルに対して、CubePDF で取り得る全ての設定でテストする事 を目的としていたのですが、1 つのテストケースに対して 1 分以上の時間を要すると言うとんでもないテストコードとなりました。

テストを実行して成功した後、小さな変更をしてテストが失敗したら、問題の原因がどこにあるかを正確に知ることができる。それは、今しがた実施した小さな変更のどれかなので、変更を元に戻してやり直すことができる。しかしテストが大きければ、実行時間は非常に長くなってしまう。このため、エラー箇所を特定できるまで頻繁にテストを実行するのを、つい避けてしまいがちになる。

Michael C. Feathers, “レガシーコード改善ガイド” (p.15)

CubePDF は、どちらかと言えばかなりテスト規模の小さな部類に入ると予想されますが、上記のようなテストケースが大量に存在していたため、毎回のテスト実行が非常に億劫になる現象を引き起こしました。この失敗で得られた教訓は、最悪でも選択的なテスト実行においては余計な時間を取られないよう、少なくとも foreach 内の各テストが単一のテストケースとなるように記述を工夫すべきと言うものでした。

Ruby を用いた Web サービスと RSpec

CubePDF のリリース後、2011 年には CubeICE、2013 年には CubePDF Utility と、利用者数的には現在でもキューブ・ソフトの主力となるソフトウェアをリリースしていますが、後から振り返ってみると、個人的な開発スタイルとしては停滞期を迎えた時期でした(もちろん、その時、その時では、頑張っているつもりでしたが)。

この頃、キューブ・ソフトでの開発と並行して個人的なプロジェクトである Web サービスの開発・運営にも取り組んでおり、この時の成果物が SoGap となります。SoGap は、複数のソーシャルメディア(ここでは、はてなブックマークTwitter 、および Facebook)のいずれでも話題になっている記事を除外する事によって、各ソーシャルメディアでのみ話題になっているニッチなものを探そうと言うコンセプトで、概要および SoGap で取得したデータを用いた解析に関しては Proposal of a New Social Signal for Excluding Common Web Pages in Multiple Social Networking Services と言う論文にまとまっています。ただ、残念ながら諸々の都合で、2015 年 11 月 20 日をもって SoGap の更新は終了しました。

この Web サービスの開発時に利用したのが RSpec でした。私自身は Spec と言う概念を正確には把握しておらず、単にユニットテストフレームワークの一つとして利用していたのですが、上手く記述すると実行した時に英語として読める形で結果が出力されていく様は、書いていて楽しいかもと思わせる魅力でもありました。

RSpec に関する記事で学んだ事は、単一のテストケースに含まれる Assert の数はできるだけ少なくすべきと言うものでした。特に、for 文中の各要素に対して Assert が実行されるような状況でテストが失敗すると、どの部分が失敗したのか判別するのが困難になるので良くないと言う指摘は、なるほど確かにと納得した事を覚えています。RSpec に関する記事では 1 テスト 1 Assert と言う主張も見られ、さすがにそこまでは厳しいですが、失敗した時に判別できるようにメッセージ等を工夫するなど、それまであまり気にしていなかった点に気付けた事は、テストと言う観点から見ても良かったと思います。

PDS とアプリケーションのユニットテスト

再び C# の開発に目を戻します。C# による開発では長年、アプリケーションに近い部分のユニットテストを何とかしたいと言う思いがあり、2014 年頃にようやく Presentation Domain Separation (PDS) と言う概念にたどり着きます。最初は非公開としているプロジェクトのリファクタリング時に適用したのですが、何度か失敗した後、公開しているプロジェクトにも適用する事に成功しました。これらの詳細に関しては、下記の記事を参照下さい。

適用したパターンは、それぞれのソフトウェア毎に微妙に異なりますが、CubeICE のリファクタリング以降は概ね View 以外はユニットテストでカバーできる形になりました。View 部分のユニットテストは相変わらずの懸念事項です。Friendly と言うフレームワークを用いた View テストの発表・実演を何度か見て、非常に良さそうと言う印象を抱いているので、折を見て試してみようとは思っています。

開発をサポートする Web サービスの拡充

2018 年現在までたどり着きました。ここ 1, 2 年、開発面で大きく前進したと実感できるのは AppVeyorCodecov と言った開発をサポートする Web サービスの存在が非常に大きいと感じます。現在では、キューブ・ソフトで開発しているソフトウェアおよびライブラリのかなりの部分まで Continuous Integration (CI) の実現に成功し、ユニットテストカバレッジ等も分かりやすく閲覧する事ができます。かつてであれば、対応するパッケージを自らのサーバにインストールする所から始めなければならなかったのが、ボタン一つで多くの作業が終了してしまうのは、利用者から見れば非常に大きなメリットです。

Codecov

Web サービスを利用する事によってカバレッジと言う指標に対する見方も変わりました。カバレッジと言う指標は必ずしもソフトウェアの品質を保証するものではありませんが、後々テストを追加していく時のための枠組みとして、取り合えずこの辺りまではやっておくと言う指標程度には利用できると感じています。また、行単位でテスト状況が確認できると言うのは非常に便利で、カバレッジと言う数値自体は置いておくとしても、この機能だけでも利用する価値はあるように思います。これらの詳細な感想については、下記も参照下さい。

以上、これまでの自分の経験を主にテストと言う観点でまとめてみました。PDS と言う概念にたどり着くまで、プログラミングを始めて 15 年程度、GUI プログラミングを開始してからでも 4, 5 年かかっている等、改めて振り返ると恥ずかしい部分も多々ありました。また、現在でも、よく分かっていないと感じる事の方が多い状態ですが、これからも少しずつ解決していければと思います。

*1:プログラムが足りないような気がするのですが、残念ながら、これ以外のプログラムをサルベージする事はできませんでした。

*2:メインとするプログラミング言語C# になった等の都合で、残念ながら、現在は保守をしていません。