2011-12-13
テスト自動化をサポートするDVCS #swtestadvent2011
このエントリは「Software Test & Quality Advent Calendar 2011」の12/13担当分です。前回の記事は「UWSC、ウチのプロジェクトではこう使ってます - shiro庵」、次回の記事は「ソフトウェアの品質を学びまくる:それでもやはり、品質を「予測」したい #swtestadvent2011」です。
はじめに
ここ数年バージョン管理システムの一形態として分散型バージョン管理システム(以下DVCS)が注目されていますが、これは自動テストと大変親和性が高いツールだと実感しています
例えば継続的インテグレーションでの継続的なタスクとして自動テストを実行する構成(以下継続的な自動テスト)でDVCSを導入すると、そこでの自動テストの構成の選択肢が大きく広がります。良い機会ですので、今回の担当分では継続的な自動テストとDVCSの組み合わせの効果についてまとめていきたいと思います。
集中型VCSの問題
バージョン管理システムについては、今でもSubversionといった集中型バージョン管理システム(以下集中型VCS)が主流です。
ただ集中型VCSで継続的な自動テストを構築しようとすると、テスト実行がコミット前であれ後であれ、変更管理で以下のような問題を抱えることになります。
コミット後にテスト実行→テストを失敗させるコミットを防ぎにくい
自動テストの運用環境では、コミット直後に共用環境側で自動テストを実行する環境がしばしばとられます。これは自動テスト実行をpost-commitに紐づける、CIサーバ等でコミットを監視するといった手段で実現されます。
ただ集中型VCSでこうした構成をとると、テスト結果がコミット後に明らかになることからテストを失敗させるコミットを防ぎにくくなります。そうなると開発が活発な間は以下のような問題が発生します。
- コミット・更新の際は、最新リビジョンのテストが失敗していないか注意を向けなければなりません。テストが失敗している間は、変更管理を諦める場面も出てきます。というのも、テストが失敗している状態でコミット/更新によりマージを実行すると、テストを失敗させる変更ミスが作業ファイルにマージされるリスクがあるためです。さらに集中型VCSではマージ前でのコミットが難しいため、そのような問題あるマージからの復帰はしばしば困難です。
- テスト失敗時の手間を予防するため、コミットの際はテストを失敗させないようにコードを作りこむ必要があります。そこでは途中で変更を記録したいと思ってもその作り込みが終わるまでコミットできません。
- 集中型VCSではマージ前でのコミットが難しいため、マージがリスクある作業となります。マージしてコミットした変更でテストが失敗した場合、自分の元々の変更に原因があるのか、マージ作業ミスに原因があるのか特定が困難な場面が出てきます。
なおこうした問題は特にテスト実行に時間がかかる場合や、テスト失敗からの修正に時間がかかる場合で深刻になります。
まずテスト実行に時間がかかる場合は、コミットからテスト結果が得られるまで結果が不明な状態が生まれるため、誤った変更がマージされたり、一定期間コミットができなくなったりする問題が出てきます。またコミット間隔よりテスト実行時間が長くなると、テスト実行時間に応じてコミットの頻度を下げる、テスト用ブランチを新規で作るといった手間が別途要求されます。
またテスト失敗からの修正に時間がかかる場合も同様です。そこでの修正中、他の作業者はコミットやアップデートがしづらくなり、変更管理をある程度諦める場面が度々出てきます。
コミット前にテスト実行→こまめな変更管理が困難となる
一方、上記問題を避ける手段として、コミット直前にテストを実行する構成が取りえます。例えばこれはpre-commitに自動テストをフックする、開発環境やビルドプロセスにテスト実行を組み込む、手動で自動テスト環境を動かすといった手段で実現されます。こうした環境を構築すると、集中型VCSでもテストを成功させたコミットのみを共有リポジトリで管理できるようになります。
ただこうした構成では、テストを成功させるまでの変更管理が困難となるという制約を抱えています。これらは以下のような問題を生みだします。
- テストに失敗した場合、元々コミットしようとしてた変更に、テスト失敗を解消するための修正変更を加えることになりますが、そこでの変更管理ができません。修正で元々コミットしようといた変更が破壊されても、元に戻すことがしばしば困難になります。
- マージ前のコミットが難しいため、マージがリスクある作業となります。マージしてコミットした変更でテストが失敗した場合、他者の変更に原因があるのか、マージ作業ミスに原因があるのか特定が困難な場面が出てきます。
いずれにせよ自動テストとコミットを紐つけると、それがコミット前で実行するか後に実行するかに関わらず、テストをパスできると十分判断できるまでコミットしにくくなります。
当然ながらバージョン管理はテストをパスさせる作業のみに使われるものでないため、そうした制約は負担を各所で発生させることになります。
集中型VCSの問題の対策の問題
なお集中型VCSでも、デファクトスタンダードとなっているだけに、上記問題に対する対策も存在します。ただそれらはDVCSと比べ相対的な問題を抱えがちです。いくつかの例を以下に挙げます。
ブランチを活用→ブランチ管理が煩雑化する、軽快にブランチを作れない
「テストを失敗させるコミットを防げない」「テストをパスできるまで完璧に作り込まないとコミットできない」といった制約を回避する手段として、しばしばブランチが使われています。
例えばコミットはとりあえずサブブランチに対して行い、そこでテストをパスしたこと確認できたらそれらをメインブランチに統合するというワークフローがその一つです。そこではサブブランチに自由にコミットできつつ、メインブランチは常にテストがパスされた状態を維持できます。
ただ集中型VCSでは、そうしたワークフローをとると以下のような問題にしばしば直面します。
- ブランチ管理が煩雑となります。ブランチはすべて共通リポジトリ内で行われるため、共有リポジトリ内にクローズされないブランチが散在しがちです。特に「テスト失敗のため退避ブランチを作る」と言った軽快なブランチ作成を行うと、リポジトリ内のブランチが無秩序に増殖することになります。
- ブランチの切り替えが煩雑で多用できません。
- 例えば一般的な集中型VCSでは、アップデート/マージは安定版ブランチから、コミットは作業用ブランチへ、といった柔軟なブランチ切り替えが簡単にできません。そのためブランチ分けしても、実際は作業用ブランチにコミットする作業フェーズと、作業フェーズでの変更を安定版ブランチに反映させる安定版更新フェーズを完全に切り分けて、それらを時系列順に切り替えていく方法を採りがちです。そこでは作業フェーズと安定版更新フェーズは同時並行している訳でないため、「集中型VCSの問題」で挙げた諸問題が作業フェーズ等で度々発生します。
- また柔軟にブランチを作れません。例えば「マージが怖いからブランチを追加して現在の変更をそちらに退避する」といった一時しのぎ的な操作はしばしば軽快に実行できません。
変更の共有→競合のリスクを高める
一方、マージに関わる問題回避のために、事前の変更共有という手段もとられます。例えばパッチはその手段の一つです。また多くの集中型VCSでは並列するブランチ間の変更共有機能も存在します。
ただ集中型VCSでは、しばしば変更共有機能が貧弱なため、事前共有した変更が競合化しやすいリスクをもっています。前倒しで変更を共有すると、そうした無用な競合を避けるためかえってコミットのタイミングや規模に制約が生まれる可能性が出てきます。
DVCSの効果
このように集中型VCSでは自動テストとの紐づけに関していくつか問題を抱えているのですが、DVCSでは若干の構成管理の手間を加えることによって、そうした問題を軽減・解消することができます。というのも、以下に挙げるような構成・工夫を容易に実現できるためです。
プライベートリポジトリと共有リポジトリの分離
基本的な構成ですが、DVCSでは下図のように作業者ごとのプライベートな環境と共有環境でリポジトリを分離することができます。ここでは通常の開発はプライベートなリポジトリへのコミットで変更管理します。そして作業完了など、切りのいいタイミングでプッシュを実行してプライベートなリポジトリに格納されている変更を共用リポジトリに反映させます。
こうした構成をとると、各作業者は共用リポジトリに紐付けられた自動テストを意識することなく、自由に作業の変更管理ができるようになります。例えば自動テスト実行前の状態をコミットしたり、マージ実行前の状態をコミットしたりすることができますし、自動テストを失敗させるかもしれない変更も任意にコミット可能です。これによってよりバージョン管理の恩恵を受けられるようになり、特にマージ作業や自動テスト失敗からの修正作業が大きく効率化されます。また自動テストが失敗していたり、あるいは使用不能になっている状態でもバージョン管理が可能となります。
共有リポジトリの階層化
DVCSではさらに共有環境上でのリポジトリの階層化も一般的に容易に可能です。
例えば下図のように、共用環境側でテストの入力となる変更を管理するリポジトリと、テストをパスした変更を安定版として管理するリポジトリを分けて構築することができます。
ここでは通常の変更はテストの入力となるリポジトリにコミットしつつ、他の作業者の変更は安定板リポジトリからプルorマージすることで取り込んでいきます(一般的なDVCSではそのようなリポジトリの切り替えを容易に実行可能です)。また両者のリポジトリでの変更共有は、Gate Keeper役を置いたり、あるいは自動テストに紐付けてコミットを自動化したりする等の手段で実現します。
こうした構成を構築すると、さらに複数作業者の間での変更共有を、自動テストの結果に依存せず実行できるようになります。すなわち前述した「作業フェーズ」と「安定版更新フェーズ」を同時並行で進められるようになります。
コミットの整形
またDVCSでは手軽にコミットを整える手段をいくつか持っています。
例えば一般的にブランチ管理が容易なことから、下図のような構成でプライベート環境にてブランチを活用すると、共用リポジトリに対して、複数のコミットログを1つのブランチにまとめてコミットすることができます。
また集中型リポジトリではリポジトリ破損リスクのため滅多に使えなかったいわゆる歴史改変(コミットの破棄等)も、プライベートなリポジトリでは自己責任において柔軟に活用できます。そのため間違ったコミットの差し戻しや冗長なコミットログの破棄を行って、共有リポジトリに対しては整ったコミットを行うことが可能です。
こうした手段でコミットの整形を行えば、プライベート環境では自由にバージョン管理しながら、共用環境に変更を送るときは自動テストを意識しつつコミットの粒度や間隔を調整できるようになります。こうした工夫はテスト実行のタイミングを調整したり、チケット駆動開発のようなコミットに規律やルールを求めるプラクティスを導入する際に有効に働きます。
まとめ
上記のように、DVCSは作業者による自由な変更管理と、自動テストと連携した規律の要求される変更管理を共立させます。結果、自動テストの運用体制の自由度を広げることができます。
一方DVCSは集中型VCSとしての運用も可能性ですし、多少の制約もありますが集中型VCSリポジトリに対してクライアントとしても導入することもできます(Subversionリポジトリを扱えるBazaarや、git-svnといった拡張等)ので、あえて導入しない理由も意外と多くありません。継続的な自動テストを考えているのならお勧めできるツールです。
2011-12-08
TDDのはじめかた #TddAdventJp
本エントリはTDD Advent Calendar jp: 2011の12/8の担当分の記事で、id:t-wadaさんの「右手に感情、左手に数値 - カバレッジを味方にしよう - t-wadaの日記」に続くものです。
はじめに
TDDはシンプルな原則に則った手法ですが、とっつきの悪さもしばしば持たれがちです。また一連のTDD Advent Calendarで起こった議論や会話の中でも、TDDの始め方はどうすれば良いかという話が散見されましたので、自分の担当枠では「TDDのはじめかた」についてまとめたいと思います。なお紹介するのはあくまで数ある入門方法のうちの1つです。たぶん他にも色々な良い入門方法があると思います。
全体像
紹介する入門方法は以下のようなステップバイステップの構造となります。
- いつでも軽快に使えるユニットテスト環境を構築する
- 必要と感じたらすぐテストを活用する
- テスト並行を目指す
- 継続的テスティングを目指す
これは全て完了しないとTDDを導入できないわけではありません。ステップを進めれば進めるほど、TDDがやりやすくなる感じです。もちろんステップの途中からTDDを始めても問題ありません。
なおこうした事前ステップをなぜ必要とするのかについてですが、これには2つ理由があります。
- まずTDDの前提スキルを無理なく身につけるためです。TDDでは製品コードとテストコードをすばやく切り替えながら開発を進めるため、軽快にユニットテストを書ける環境や技能が特に重要です。そうしたものを事前準備できていれば、TDDを無理なく実践できるようになります。
- またTDDのメリットをより活かすためです。「 TDDを学ぶべき10の理由 #TddAdventJp - やさしいデスマーチ」の通り、TDDは様々なメリットを持っていますが、それらはTDDの外にも良い効果をもたらします。広範なユニットテストの活用環境を導入しておくと、それらメリットをより活用できるようになるため、TDD導入の費用対効果が改善します。
ではそれぞれのステップを順々に説明していきます。
準備
1.いつでも軽快に使えるユニットテスト環境を構築する
まず最初に、思いついたらすぐテストを書いて実行できるユニットテストの環境を構築しましょう。
これはTDDと親和性のあるユニットテストのテスティングフレームワークの導入で実現できます。なお「TDDと親和性あるテスティングフレームワーク」とは、具体的には以下のような特徴を持つものが該当します。
- 軽快にテストを実行できる。TDDにとって、実行に1秒以上かかるテストはもう遅すぎます。そのような実害ある遅さの直接的原因になるフレームワークは避けた方が無難です。
- 簡潔なテストコードでテストを実装・実行できる。Assertion MethodやTest Methodなどの構文が簡潔で、またテストを実行するためのコードもコンパクトに済ませられるものが推奨されます。例えばテストディスカバリ機能(テストメソッドを書いたら自動的にそれをピックアップして実行してくれる機能)はあると望ましい機能の1つです。
- 明快かつ軽快にテスト結果を表示できる。テスト結果を通知するUIは、REDかGREENかの把握を一瞬で済ませられるように、色や配置等が工夫されていると効率が向上します。
- 製品コードの開発環境と連携可能。TDDでは製品コードとテストコードの開発を頻繁に切り替えていくことになりますので、それらの編集領域を簡単に切り替えられるような環境が望まれます。例えばIDEに組み込み可能なフレームワークは有望な選択肢です。
なおTDDと親和性の高い定番のテスティングフレームワークも存在します。例えばEclipse×JavaならJUnit、Visual Studio×C#ならNUnitが主流です。また言語ごとではC++ならgoogle testが、RubyならRSpecなどが有望でしょう。そうしたものを開発環境に導入すれば、無難に環境を構築できます。
ウォームアップ
2.最初のステップ:必要と感じたらすぐテストを活用する
環境を構築できたら、まず「必要に感じたらすぐにユニットテストを書いて実行する習慣」の実践を始めてみましょう。具体的には以下を日々の開発に取り入れてみてください。
- バグがありそうなコードを書いたら、とりあえずバグがないことを確認できるまでユニットテストを書く。
- リファクタリングを行うときは、コードの動きが変わらないことを確認するテスト(回帰テスト)としてあらかじめユニットテストを書く。それでリファクタリング前後でデグレードが起こっていないことを確認する。
- よくわからないコードがあれば、とりあえずユニットテストを書く。テストを継ぎ足していくことで、そのコードがどのような動きをするか、きちんと動いているか確認する。
- バグが出たら、とりあえずユニットテストを見直す。テスト上でバグを再現できないか調べたり、バグの原因をテストで絞り込んだりする。
なおここでのテストは使い捨てでも構いません。とにかく不安なコードやリスクある作業に直面したら、ピンポイントでテストを書いて即時にリスクや問題を軽減していきます。またテストの網羅度も、慣れないうちは主観的な判断で調整してしまって大丈夫です。例えば不安がなくなるまでテストを書くというのはスタートアップ段階では考え方としてありでしょう。
この習慣の実践では、対価として「テストを書く労力」と「最低限のテスタビリティを確保する労力」が要求されます。
ただその労力と比べて、この習慣で得られるメリットははるかに大きいです。追加・変更・解析・デバッグといった諸々のプログラミング作業の中で、バグやミスを防止できるようになります。また不安やリスクを早期解消できるので、より良い精神状態でプログラミングを進められるようになるでしょう。TDDの有無に関わらず、身につけておいて損はないスタイルといえます。
3.テスト並行を目指す
「必要と感じたらすぐテストを活用する」習慣を実践できるようになったら、テストが必要と感じる閾値を下げて、より多くテストを書くようにしましょう。具体的には、「明確な必要性に応じてテストを書く」から「少しでも必要性を感じれば予防的にテストを書く」(例えば「もしかしたら問題があるかもしれないからテストを書く」「将来的に問題がでそうだからテストを書く」)へ考え方を転換していきます。
こうした考え方の転換のゴールはテスト並行(Test-Concurrent)です。テスト並行は、テストを書くタイミングが後になろうと先になろうと、とりあえず製品コードと並行させてテストを書いていく考え方です。さらに生み出したテストは、製品コードと一心同体のものとして、後の変更やリファクタリングでも再利用していきます。
具体的には、とにかく可能な範囲で以下を実践していきます。
- 製品コードを追加する際は、きちんと追加できたかユニットテストで確認します。
- 製品コードを変更する際は、変更で変えたいふるまいと、変えたくないふるまいそれぞれにテストを書きます。変えたいふるまいに対しては、コードを追加する際のテストできちんと変更が実現できているか確認します。変えたくないふるまいには回帰テストでデグレードが発生していないか確認します。
- 既存のテストに問題があれば、テスト設計やテストコードの再構築を後付けで行います。
- 製品コードに変更が発生した際は、妥当であればテストもそれに同期させて更新します。
こうした習慣を続けると、以下のようなメリットが増大していきます。
- テストの作業ミス・バグの予防効果を継続的に享受できます。
- テストをペアとしてもつ製品コードが増えています。それら既存のテストの再利用により、変更やリファクタリング作業をより少ない労力で実施できるようになります。
ただ一方で、継続的にテストを書いていくと、単純にテストを書く労力が増える以外に、3つ問題がデメリットして浮上します。
- まずテストを継続的に再利用していくに伴って、テストを保守する労力が増加します。たとえば製品コードのインターフェースが頻繁に変更されると、それにあわせてテストコードも頻繁に変更しなければならなくなります。
- 次に、テスト設計の整合性がより強く要求されます。網羅性が曖昧で何をテストしているかわからないテストコードは、再利用時にバグを見逃すリスクを増加させます。またテスト設計が不適切なままテストの網羅性を上げようとすると、しばしば乱雑なテストの山ができ、テストの保守をより困難にします。
- 最後に、テスタビリティが広く求められます。特にテスタビリティが粗悪なレガシーコード相手を相手にした場合、テスト書けるようにするための修正で大変な労力と精神力を消費することになります。
こうしたデメリットは、悪化するとユニットテストの導入意義にもかかわりますので軽減を図っていかなければなりません。以下のような対策が必要となります。
- テストの保守性を作りこむ。例えば保守しやすいようにテストを書く(重複を避ける、よい名前を使う、小さな単位に分割する)、変更の影響が限定されるように堅牢なインターフェースを製品コードで確保する、等。これに関しては以下の書籍にid:t-wadaさんが書かれた良い記事があります。
- テストコードの実装プロセスを工夫する。例えば詳細なユニットテストは落ち着いてから実装し、それより以前のテストはピンポイントで必要に応じて書き捨てていくなど。これについては拙著ですが以下で一部触れさせて頂いています。
- より上位の設計からのテスタビリティの作りこむ。変更が発生しても影響範囲が限定されるアーキテクチャを構築するなど。これに関しては以下に良い示唆が含まれています。
- 適切やテスト分析、テスト設計の手法にのっとってテストを設計する。例えばコードカバレッジを評価する、同値分割やドメイン分析、デシジョンテーブルなどで仕様を整理するなど。適切なテスト設計手法に基づいてテストを作っていけば、コンパクトかつ網羅的なテストが得られます。これに関しては以下の書籍が参考になります。
なおユニットテストの習熟が進めば、上記メリットは大きく、デメリットは小さくなってより広範囲にテストの恩恵を受けられるようになります。うまくメリットとデメリットのバランスをとりながら、継続的にデメリットを下げる対策をしていきましょう。テスト並行はそれをやるほどの価値を持っています。
(応用のステップ)4. 継続的テスティングを目指す
このステップはTDD入門としては、必要性はそんなにありません。効果はあるのもの、TDDと離れた作業の比率が増えるためです。
テスト並行を広い範囲で適用できるようになったら、作ったテストを頻繁に自動実行する継続的なテスティングを目指していくのがひとつの手です。これはJenkinsといったCIツールを導入し、そこにテストの実行環境を組み込むことで実現できます。
今回は詳細は割愛しますが、継続的なテスティングを実現すると、テストを実行可能な状態に、またテストをGREEN状態に維持するためのサポートを自動化できるため、テストの保守が容易になります。結果、ユニットテストをどこでも・いつでも活用できる状態を確保しやすくなり、製品コードの変更・保守が格段にやりやすくなります。
ただ継続的なテスティングの実践ではテストの保守性がより強く要求されるため、「テスト並行を目指す」で書いたデメリット軽減策をさらに推進していく必要があります。
TDDの導入
TDDをはじめる
さてTDDですが、少なくとも「必要と感じたらすぐテストを活用する」習慣を実践できていれば導入の準備はできています。また「テスト並行」がある程度できていれば、それに比してTDDを活用できる範囲も大きくなっているはずです。すでにTDDを解説する世の中のウェブや書籍で、スムーズにTDDを習得できるようになっていると思います。
なお参考資料としては、とりあえずこだわりがなければ以下の本が定番です。本エントリで長々と解説するよりこの本を読んで頂いた方が色々無難かと思います。
なお本格的な解説は上記書籍にお任せするとして、残りはとっつきの悪さや誤解が少なくないテストファーストの導入について、補足資料として簡単に解説します。
テストファースト
最初の単純な考え方の1つとして、テストファーストは作業を細切れにする手法と考えると混乱が少ないと思います。
例として、ピンポイントのテストであれテスト並行であれ、テストが必要そうなコードを新規に書く機会があったら以下を実践してみましょう。
- 書こうとしている製品コードの仕様を細切れにして(前述の書籍「テスト駆動開発入門」ではこれをリストアップしたものをテストリストと呼びます)、小さな仕様をピックアップします。
- ピックアップした小さな仕様についてテストを書いて失敗させます。
- 製品コードを書いてREDをGREENに変えます。
テストの大きさは、なるべく一息でGREENにできるような細かい粒度にするのが無難です。例えば「4で割り切れる年は閏年、100で割り切れる年は平年、400で割り切れる年は閏年」と判定するうるう年判定関数を実装する場合は、以下のようになります。
- 「西暦年が4で割り切れる年は閏年」と小さな仕様をピックアップして、
- 「4で割り切れる年は閏年と判定する」ことを確認するテストを書いて失敗させ、
- テストを通す最小のコードを書いてテストをGREENにします。
こうした流れでは、最初のREDで次に進める作業範囲を明確化して、次にRED→GREENで作業が完了していることを明確化しています。これにより自分の作業に対する直感的かつ即時のフィードバックと、作業の自己統制感、そしてきちんと作業をこなしたという安心感を確保します。またテストを使ったことによる副産物として、それなりのテスタビリティと、テストコードを確保します。
なお前述「テスト駆動開発入門」では、「Fake It」とリファクタリングというアクティビティが解説されていますが、これは作業の粒度調整手段ともいえます。例えばFake Itは「インターフェースを実装する作業」と「内部実装を行う作業」を細かく分離する手段として使われます、またリファクタリングも、「必要な機能を実現する作業」と「完成されたコードを実現する作業」を細かく分離する手段として有効です。
最後にここで書くテストはあくまで作業確認のためのテストです。作業単位で累積していくだけですので、網羅性や全体整合が取れない場合もしばしばあります。網羅的なバグ出しなど、他の用途でテストを再利用したいならば、作業の目途がたったところで別途テストを再整理する必要があります。
設計のためのテスト
テストファーストができるようになったら、今度はテストに設計やふるまいを洗練させる視点も取り入れてみましょう。流儀は複数ありますが、今回は製品コードの使い勝手にフォーカスした例を以下に示します。
- テストコード上で実際に製品コードのインターフェースや呼び出し方法を書いてみます。
- 製品コードのインターフェースや呼び出し方法について、どのようなものが良いか考えます。
- テストを実行してRED状態にし、TDDにより製品コードを実装します。
こうした内部実装に入る前に一呼吸おいてインターフェースや呼び出し方法を考えるアプローチは、製品コードのテスタビリティや堅牢性、使いやすさを向上させる点で大変有効です。なおこれをより上位の呼び出し元で実践したものはOutside-In TDDと呼ばれます。
経験を積む
なおTDDは原理主義的に実践しなくても大丈夫です。あくまでテスト並行をベースに以下のようなフローをぐるぐる進めていきましょう。
- テストが書けないなら...
- テストが書けるなら...
- バグのリスクや分かりにくさを感じたら...
- テストを継ぎ足してリスクや問題を軽減する
- テストに問題があれば...
- 追加でテスト設計やテストコードの再整理を行う
とにかく呼吸するようにユニットテストを活用していきましょう!それによりTDDが使える機会は増えますし、TDDの恩恵も倍増していきます。そしてそれを続けていけばさらにTDDの習熟が進んでTDDの費用対効果が上がり、TDDを活用できる機会が拡大します。
まとめ
長くなってしまいましたが、まとめると以下のようになります。
- ユニットテストを軽快に使いこなす習慣を実践していると、TDDを楽に導入できるようになります。
- TDDは一律に実践しなくても大丈夫です。呼吸するようにユニットテストを活用する中で、活用できる領域でガンガン実践していきましょう。
以上、TDD入門で詰まったときに本エントリが役立てば幸いです。
つぎは
明日の担当は id:yujiorama さんです!
2011-10-17
テスト自動化の目的
最近あるMLでテスト自動化の目的について考える機会があったのですが、今回は整理としてそこで言及したことをまとめたいと思います。
色々な目的
よく言及されていますが、テスト自動化の目的は単に「人がやっていることをツールにやらせて楽をする」といったものに限りません。思いつくものでも、例えば以下があります。
- 繰り返し作業を効率化する
何度も繰り返す作業を自動化して、繰り返しによる作業重複分を効率化します。- 継続的なテストの実現
テストの繰り返し実行を容易にして、高頻度の回帰テストを実現します。例えばCIへのテストの組み込み等を実現します。 - 時系列の評価の効率化
テストの評価(カバレッジや進捗評価等)を頻繁に実行可能にして、時系列の推移の評価を容易にします。例えばコードカバレッジの低下を監視して、ユニットテストの劣化を評価する等します。 - テストの維持
テストのビルドエラーや実行エラーの高頻度なチェックを実現して、テストを常時使用可能な状態に維持するため保守作業をサポートします。 - 組み合わせやカバレッジの拡充
実行手順を自動化することによって、テスト実行の入力(テストデータや操作、設定等)の数や組み合わせを容易に増やせるようにします。
- 継続的なテストの実現
- 複雑な作業を簡略化する
複雑で手間のかかる作業を自動化でサポートします。- 手間のかかる実行手順を簡略化
人手がかかる作業を自動化でサポートします。 - テストデータの簡略化
テストデータの自動生成を行うことで、テストデータの抽象化・簡略化を実現します。 - テスト設計の簡略化
テストケースの自動生成を行うことで、テスト設計作業を簡略化します。例えば状態遷移表や原因結果グラフから自動的にテストケースを求める等します。 - テスト設計の入力の簡略化
テストケースの自動生成を行うことで、テスト設計の入力の抽象化・簡略化を行います。例えばセキュリティ検証ツールの導入でソフトウェアによらない標準的な検査を追加したり、あるいは非常に簡単なモデルや仕様記述言語から冗長性を持つテストケースを生成させたりします。
- 手間のかかる実行手順を簡略化
- 人間の曖昧さを排除する
自動化によりミスや判断のぶれ、属人的な偏りを軽減します。
- 時間・環境の制約を回避する
自動化によってテスト実行上の時間的・環境的な制約を緩和します。
自動化の目的の軸
上記のようにテスト自動化の目的は様々ですが、整理の軸として観点を絞ると考えやすくなります。例えば以下のような観点を用いると、自動化の目的を整理できるようになります。
テストプロセスのフェーズ
テスト実行の4フェーズ
- Setupを自動化する
- Exerciseを自動化する
- Verifyを自動化する
- Teardownを自動化する
ロール
- 開発者をサポートする
- 検証者をサポートする
- 管理者をサポートする
既存のテスト/新規のテスト
- 既存の作業を自動化する
- 自動化により新たな作業を実施可能にする:例えば人手が使えない夜間実行を実現する等
作業そのもの/作業の繰り返し
- 作業を効率化する
- 作業の繰り返しを効率化する
さいごに
テスト自動化を活用できる領域は現場によって様々なので、上記のリストは一律に適用できるものではありません。ただできること(目的)とそのやり方を広く把握しておくと、問題解決や改善の糸口にはなると思います。
2011-10-12
TDD Boot Camp東京 for C++に登壇
先週末、TDD Boot Camp東京 for C++にTAおよび講演者として参加させて頂きました。主催の @imagire さん、登壇者として声をかけて頂いた @t_wada さん、会場提供して頂いた @mnagaku さんをはじめ、TA、参加者の方々には深くお礼申し上げます。
講演について
今回は入門者のための応用の講演ということで、TDDを実践し始めた人がぶつかるハードルへの対策を解説させて頂きました。アウトラインは以下のような感じです。
- 実践のネクストステップ
- テストを整える
- 変更に備える
- 変更に対処する
- 学習のネクストステップ
- 基礎を身につける
- より活用する
- 応用分野を学ぶ
具体的なテーマとしては、TDDで作ったテストの保守、テストの再利用、プロダクトコードの変更への対応、(時間がなく講演中に省略しましたが)テスト困難なコードの扱い方について扱わせて頂いています。また補足として、後半にてTDDの勉強の手掛かりを複数紹介させて頂きました。
講演資料:テスト駆動開発入門ネクストステップ
(番外)課題について
今回は最短経路探索アルゴリズムという複雑な処理を扱わざるを得ない課題が興味深かったです。というのも、そういった難解な処理でTDDの効率を確保しようとすると、TDDの進め方にそれなりの工夫が要求されるためです。
とりあえず補足として、今回その問題について簡単に触れようと思います。まず講演内容として想定していましたが諸々の都合で没にしたスライドを転載します。
TDDの適・不適
上図はTDDの適・不適の解説の中で、単機能テストの実装作業とプロダクトコードの実装作業の関係を示すために作ったものです。
このうち左のような構造はTDDに適している構造です。テストとプロダクトを細かいステップで交互に構築できるので、効率良くテストがプログラミングをドリブンできるようになります。
一方、最短経路探索アルゴリズムの構築もそうですが、右図のように複雑に絡み合ったプロダクトコードの塊に対しては、以下のような要因からTDDが難しくなることがあります。
- RED→GREENの粒度が大きくなり、テストの効果が断たれる期間が長くなる。
- 特定のプロダクトコードに多数のテストが過依存する構造になりがちで、テスト失敗の原因特定が困難となる。
- テストファーストやリファクタリングに必要なテストの規模が増大する。
そこでは例えテストファーストは実現できても、しばしばテストがプログラミングをうまくドリブンできません。
これと似たようなものとしては、他には組み込みの制御理論といった曖昧で複雑な計算処理や、ユーザインターフェースからユニットを検証するような上位インターフェースからの間接テスト等が挙げられます。例えば前者では、学術計算ソフト等で理想値シミュレータを構築し、それに近づくように処理を作りこんでいくアプローチでテストファーストのステップを実現できるものの、やはり前述した要因によりテストがプログラミングをドリブンする形の実現が困難です。
なお話がずれますが上記の複雑な処理をライブラリを使って実現する場合は、対応が大きく様変わりします。そこでは1つの手段として、ライブラリの仕様化テストを積み重ねることで望む機能を確保していくことになるでしょう。
絡み合ったプロダクトコードの塊への対策
こうした絡み合ったプロダクトコードの塊に対してTDDを効率よく適用するには、事前設計やプロトタイピングで、ある程度テストの見通しを広げておく必要があります。またTDDを柔軟に崩すという選択肢も十分あるでしょう。
なお仮にTDDを崩すという対処をとっても、テスト・開発の並行やこまめなテストは重要です。例えばある程度コードが確保されるたびに網羅的に潜在バグを出すようにテストを補強する等しましょう。
ちなみに今回はそうした工夫をTAながら様々な流儀で見れたのが面白くまた楽しかったです。
さいごに
なお今回は資料、特に後半部分を作る過程で、TDD Boot Campの影響の広まりを具体的に認識させられたのが印象深かったです。例えばTDDBCはSCM Boot campのような派生コミュニティを生んでいますし、他の要素とのコラボによるものも多いでしょうが、DVCSとの絡みやテストの活用など色々な関連技術に影響を与えています。これに関しては、もしTDD Boot Campやt_wadaさんの存在がなかったらと思うと感慨深いものがあります。
2011-09-18
プログラマの読書会と3年
色々忙しい時期が続き報告にかなり間が空いてしまいましたが、少し前にxUnit Test Patterns読書会の最終回に参加させていただきました。
このxUnit Test Patterns読書会、前のWorking Effectively with Legacy Code読書会から連続して続くもので、初期のころから参加させていただいています。プログラマの読書会コミュニティとしては初めて参加した勉強会であり、かつ参加してから丁度3年経過したということで、今回ちょっと振り返ってみたいと思います。
WEwLC読書会との出会い
この読書会への参加は、流し読みしていたIT勉強会カレンダーでたまたま「Working Effectively with Legacy Code」という書名とその第一回読書会を見つけたのがきっかけとなっています。当時レガシーコードに悩んでいたこともあり最初は個人で読み進めていましたが、結構読みづらい洋書だったので第二回より参加を申し込むことになりました。プログラマの勉強会コミュニティへの初めての参加だったため、申し込みにも参加にもかなりびくびくしていた記憶があります。
ただその読書会、蓋を開けてみると自分の予想外の方向に飛んでいる勉強会でした。
というのも、対象書籍は「テストのないコードはレガシーコードだ」と言い放つユニットテスト/テスタビリティ原理主義の本であり、参加者も id:t-wada さん id:kunit さん @tosikawa さん id:setoazusa さんといったTDDやユニットテストの先駆者・実践者が複数参加されていました。さながらDeveloper Testingのディスカッショングループのような様相を呈していたと思います。
また今でこそ生活の一部にとけ込んでいますが、プログラマコミュニティの「勉強会」そのもののインパクトも衝撃的でした。特に当時はソフトウェア開発についての語り合いや議論に飢えていただけに、プロフェッショナルな人達と交じりつつ、時には酒を飲みながらプログラミングやテストについて語り合うその場に、のめりこまない訳がありませんでした。
そしてそれからそのエクストリームな本をエクストリームな人達と一年間読み進めた結果、未熟で勉強会コミュニティに対してもほぼ無防備だった自分は、完全にこのWEwLC/xUTP読書会に染められることになりました。
読書会と三年
技術書の良質な読書会というのは本の学習効果を何倍にも増幅してくれます。
例えば本会では、議論や解説を通して文章に補足・具体例・現場での導入方法・反論などといった情報ががどんどん紐づけられていきます。またTwitter、チャット(WEwLC読書会ではLingrが愛用されていました)、ML、Wikiといった読書会のコミュニケーションツールを通じて、多くの補足資料やリンクが記録されていきます。さらに単純に自分の担当枠については、準備作業を通してかなり詳細な知識が身に付くことになります。
そして結果的に、いろいろな視点で深堀りされ、かつ体系だった知識が頭に定着していくことになります。
自分にとって幸運だったのは、WEwLC/xUTP読書会がそうした良質な読書会であり、かつそこでWorking Effectively with Legacy Code、xUnit Test Patternsという良書を読み進められたことです。
例えばWEwLC読書会では、テスタビリティの作りこみや現場との向き合い方について多くを学べました。特に著者や読書会参加者のテスタビリティ至上主義は、自分の開発・プログラミングスタイルに少なからぬ変化を与えたと実感しています。また当時は伝統的なWFのテスト設計・テストプロセスに従事していたこともあって、現場でのテストの活用のレパートリーが一気に広がることになりました。
そして続くxUTP読書会でも二年間を通してユニットテストの体系的な知識を身につけることができました。特にこの本ではテストの保守性について多くの示唆や問題意識を喚起されましたが、それらはのちのち講演や執筆の機会を頂くような流れの原動力となりました。
一方、そこでのエンジニアの方々との交流から得られたものも少なくありません。例えば参加者の @tosikawa さんからはソフトウェアテストの話を色々伺う一方でTEFやWACATEの紹介をして頂き、以後のテスト方面への活動の着火点につながりました。また @t_wada さんは当時でも圧倒的で、(緊張してまともに話せてませんでしたが)とにかくt-wadaさんがテストの講演をすると聞けばDevLove、オブラブ、XP祭り、デブサミと足を運びました。ややフリーク的でしたが、そこで学んだ知見は今の自分のTDDの基礎につながっています。さらに id:htada さん、 id:tenkoma さんらは勉強会やカンファレンス等で毎回のように会って議論などをするイベント仲間になりました。
また読書会が開催された三年間の間では、 id:setoazusa さん、id:takagimasahiro さん、その他多数の参加者の方々が、雑誌や本を執筆されたり、デブサミ等で講演されたり、公的な賞を受賞されたりと、次々と活躍していくのもとても刺激的でした。それに喚起されて、そのうち自分もブログを書き、LTで話し、講演や記事執筆にも手をつける、のような流れで、自然にアウトプットの場を広げるようになりました。
振り返って
この読書会が突出して優れたコミュニティかというと、もしかしたらそうではないかもしれません。タイミング、人、相性等が自分にとってとても良かったのも大きいと思います。
ただ、背景がそうであっても、そこで得られたものは膨大です。例えるなら本から芋づる式で世界が広がっていくような3年間でした。参加のきっかけは「Legacy Code」という単語に引っかかるという些細なものでしたが、結果として自分は大きく変わりましたし、その3年間は自分のエンジニア人生の中で大事な思い出になっていくと感じています。
読書会は三冊目の「Growing Object Oriented Software, Guided by Tests」に移っていますが、これからも気長に付き合えていければなんて感じています。 主催の@setoazusa さんや参加者の方々には本当に感謝しています。








