構造を決定するテスト

2018年4月26日(木)
kyon_mm

はじめに

今回は前回に引き続き、「構造を浮き彫りにするテスト」と「構造を決定するテスト」のうち「構造を決定するテスト」について解説します。

構造を決定するテストは、これから作るものの形や手触りがこうあるべき、と定義します。これにより、本来あるべき形や手触りからずれることなくソフトウェアを開発できます。これらのために具体的に利用するプラクティスについて解説します。

まず、テストによってソフトウェア構造を決定する体系として「TDD」について解説し、その後、部分的に開発を進めながら全体としてソフトウェア構造を決定する方法として「テストダブル」について解説していきます。

構造を決定するテストとして体系化されたもの

構造を決定するテストには、「設計活動として取り組んでいく活動のもの」と「構造を浮き彫りにするテストによって発見されたものを精査して変化させる活動のもの」と大きく2通りがあります。後者の具体例は前回解説したので、今回は前者について解説します。

設計活動として取り組む構造を決定するテストは、広義のソフトウェアの形式的定義です。ソフトウェアの形式的定義をより細かく分類すると、次のようになります。

  • TDD/BDD/Specification by Example
    ‐テストファースト
    ‐テストダブル
    ‐リファクタリング
  • 形式仕様記述
  • 形式検証
    ‐定理証明
    ‐モデル検査

この中で、今回はTDD/BDD/Specification by Exampleを中心に解説します。日本語では「テスト駆動開発」「振る舞い駆動開発」「例示による仕様」などと呼ばれているものです。これらは、ソフトウェアテストをプログラムとして記述してからソフトウェアを実装するというプロセスを繰り返していきます。まずこれから実装するソフトウェアをテストとして定義することで、ソフトウェアの構造を大小さまざまな粒度で決定します。

この他にも、VDMなどに代表される形式仕様記述や、CoqやAlloyに代表される形式検証などの技法もあります。これらはさまざまな数理的、論理的なアプローチによってソフトウェアの仕様を定義・検証できるようになっています。

TDD/BDD/Specification by Exampleの実行ステップ

TDD/BDD/Specification by Exampleと3つありますが、より良い名前を模索してきた結果がTDDからBDDになり、Specification by Exampleという技法としてまとめあげられた経緯があります。BDDやSpecification by Exampleとなる過程で、より具体的な実践例が示されてきました。実質的には同じことを示しているため、以降では簡略化してTDDで統一します。

TDDは、主に次のステップで進めていきます。

  1. テストケースをテストコードとして実装する:テストファースト
  2. テストコードを実行して失敗することを確認する:レッド
  3. テストコードが成功するようにソフトウェアを実装する
  4. テストコードを実行して成功することを確認する:グリーン
  5. 1.に戻って繰り返す
  6. 内部的により良い実装があればテストコードがすべて成功している状態のままで書き換える:リファクタリング

テストコードを実装するためのツールは、対象とするテストによってさまざまなものがあります。例えば、ソフトウェアの関数を直接呼び出したり、単純なテスト実行器として使ったりするにはJUnit、Spock、RSpec、mochaなどがあります。対象ソフトウェアのプログラミング言語でテストを記述し、実行してレポートを出力します。WebAPIを呼び出してパフォーマンステストを実行するには、例えばGatlingなどがあります。Gatlingはスクリプトでテストを記述し、実行してレポートを出力します。

具体的にTDDは図1のように進めていきますが、これについて『テスト駆動開発』(オーム社刊)の著者Kent Beckは次のように述べています。

図1:TDD Cycle

自動テストが失敗した場合だけ、新しいコードを書く。重複を取り除く。2つの規則はプログラミングのタスクにおける順番を意味する。

レッド:動作しないテストを少しだけ作成する。おそらく最初はコンパイルできない
グリーン:テストをすぐに動作させる。そのためには、どのようなコードでもよい
リファクタリング:テストを動作させるためだけに作成された重複をすべて取り除く

TDDはテスト技法ではない。分析技法であり、設計技法であり、開発のすべてのアクティビティを構造化する技法である。

テストコードによって決定する構造はプロトコル(インターフェースと手続き)です。プロトコルを定義し実行して、そのとおりかをチェックするというのがテストコードの役割です。必ずしもプロトコルのすべてをTDDにおけるテストコードとして構造を決定する必要はありません。特にインターフェースに関してはJavaやScala、Haskellなど静的型付きなプログラム言語によるコンパイルチェックを活用するのも良い選択肢です。

なお、テストコードを記述しなくても構造をコンパイルでチェックできますが、これらは手続きの構造を決定することが苦手です。複数のメソッドや関数を定義したときにどのようなパラメータをどのような順番で呼び出すかを定義することは限定的にしかできません。このような部分は、テストコードを活用することで構造を決定します。

TDDだけでなく、静的型付きプログラミング言語をはじめとした他の手段も含めて、適切な方法を組み合わせることで保守性の高い設計にできます。すべてのソフトウェアコードをTDDで設計し、構造を決定することはある種の理想的な開発だと思われますが、何度も行ったことのある要件やソリューションでなければ難しいというのが筆者の経験です。作り直すことを前提としている場合には、多くのコードをTDDのような技法で設計しておくと構造を決定しやすいでしょう。

構造を決定しながら開発を進めていく場合には、大きく2つのアプローチがあります。「インサイドアウト」と「アウトサイドイン」です。「ボトムアップ」と「トップダウン」と表現する場合もあります。インサイドアウトとは、構造の一部分(小さなメソッドや関数)から作っていき、それらを組み合わせて要件を達成していくやり方です(図2)。小さく作るため作りやすい代わりに要件との不一致が起きやすくなります。

図2:インサイドアウト

一方、アウトサイドインとは、構造の大枠から作っていき、徐々にその中身も作って要件を達成していくやり方です(図3)。大きく作るため作りにくい代わりに要件との不一致は起きにくいです。

図3:アウトサイドイン

実際にはどちらかに寄せて開発するのではなく、プロジェクトの状況やフェーズによって組み合わせて使い分けます。例えば、筆者はアーキテクチャ設計やコンポーネント設計レベルではアウトサイドインでインターフェースを決定しておき、徐々にインサイドアウトで開発を進めていくという方法を採っています。

テストダブル

構造設計の正しさについては、できるだけ早くある程度のフィードバックを得られた方が、再設計を含めた修正コストは低く済みます。すべてのコンポーネントを実装していなくても、テストによって構造を決定していきフィードバックを得る方法に「テストダブル」の活用があります。例えば、開発中のコンポーネントが依存している未実装なコンポーネントについては、設計のうちいくつかを反映した仮の実装にすることで開発中のコンポーネントに注力します(図4)。

図4:開発しているコードの依存関係

テストコード上で直接やり取りするコンポーネントではなく、そのコンポーネントが"依存しているコンポーネント"を置き換えることで、構造を決定するとともに次の3つが行えます。

  • "依存しているコンポーネント"を実装することなく対象のコンポーネントを開発する
  • "依存しているコンポーネント"でわざと例外的な挙動を起こしてテストする
  • "依存しているコンポーネント"を正しく使えているかをテストする

この置き換えるという技法や置き換えたコンポーネント、オブジェクトを「テストダブル」と言います(図5)。

図5:テストダブル

テストダブル自体は「xTest Unit Patterns」で定義された言葉です。それまでは「モック」や「スタブ」などと呼ばれていましたが、それらを総称してテストダブルとしました。ダブルには映画のスタントにおける「代役」という意味があり、テストにおける代役を意味します。

ただし、こういった機能は何のために使うかを見失うと保守性の低いテストコードになり、あまりにも構造を変更しにくくなってしまう時があります。筆者はテストダブルを大まかなソフトウェアの構造を決定する目的で使い、テストダブルでなければ解決できない問題以外では利用しないようにしています。

上記で、依存しているコンポーネントやオブジェクトを置き換えると言いましたが、"ロールや責務を置き換える""ロールや責務をモックする"という感覚を念頭に置いておくと良いでしょう。あるコンポーネントやオブジェクトそのものすべてを置き換えるという意味ではなく、ある側面を置き換えるという意味です。

これはオブジェクト指向プログラミングにおける設計として重要な「RDD(責務駆動開発)」などに代表される、責務の発見や割当てによる分析技法、設計技法の考え方をベースにしているためです。責務、ロールなどの考え方は『オブジェクトデザイン』や『実践テスト駆動開発』(いずれも翔泳社刊)に詳細がありますし、モックについては『Mock Roles, not Objects』という論文で考え方がまとめられています。

「xUnit Test Patterns」をはじめとして、テストダブルは大まかに次の5種類に分類されます。

  • スタブ:あるロールが任意の値を返すように置き換えるもの
  • モック:あるロールの入出力を監視し、なおかつそれらが正しいかを検査するように置き換えるもの
  • スパイ:あるロールが出力したものを監視して後から検査できるように置き換えるもの
  • フェイク:あるロールとほぼ同じように入出力するように置き換えるもの
  • ダミー:テストにまったく影響しないロールを置き換えるもの

ロールという単語が聞き慣れない場合は、「性質」や「責務」と捉えても良いでしょう。もう少し簡単に説明すると、「スタブ」は固定値を返すもの、「モック」は呼び出しが正しいかをテストできるもの、「スパイ」はレスポンスが正しいかをテストできるもの、「フェイク」はほぼ本物と同じように動作するものです。

テスティングフレームワークによってはこれらのいくつかを提供している場合もありますし、テストダブルだけのライブラリもあります。テストダブルはある程度DIの仕組みによって実現可能なものもあるので、必ずしもテスティングフレームワークやテストダブルのライブラリによって実現しなければいけないというわけではありません。対象のプロダクトの制約に応じて使い分けます。

ソフトウェアの構造を決定するときにテストダブルを利用することで、全体の整合性がとれていることを確認しやすくなります。TDDかどうかに関わらず、さまざまなレベル(メソッドや関数、外部ライブラリ、WebAPI、DBなど)でテストダブルを活用できます。

おわりに

前回と今回の2回にわたり、システムの品質を向上するために必要な活動によってソフトウェアテストを「構造を浮き彫りにするテスト」と「構造を決定するテスト」に分けて解説しました。このうち、構造を決定するテストをTDDやテストダブルという技法によりプログラムとして実行しながら構造の全体と部分を定義していくアプローチを解説しました。

アウトサイドインとインサイドアウトを組み合わせることで無理なく矛盾なく開発を進めやすくし、繰り返すことで早く頻繁に設計へフィードバックします。TDDやテストダブルの詳細は『テスト駆動開発』や『実践テスト駆動開発』などの書籍を参考にしてください。

次回は、「開発者としてのテストの自動化」という視点で実践に向けた基本を解説します。

テスト自動化研究会 / 株式会社オンザロード
ソフトウェアテストを中心にアジャイル開発や関数型プログラミングを活かしたシステム開発に従事している。現在の主な活動は、アジャイル系カンファレンスでの講演、JaSST 東海実行委員など。主な著書に『システムテスト自動化標準ガイド(CodeZine BOOKS)』がある。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています