@634

メモ - テスト設計

Advertisement

テストデータの提供

プロジェクト初期の開発環境整備の段階で、本番と同じ(または同等)のデータを準備すること。本番に近いデータを早期に開発者に提供することで、得ることが出来るメリットがたくさんある。

たとえば、
  • 仕様書の曖昧さ、業務熟知者の暗黙値をカバーすることができる。
  • データによるレイアウトのズレなどを早期に発見することが出来る。
  • テスト時にきちんとしたデータでテストを行うことが出来るので、信頼性が向上する。
  • テーブル定義の欠点を早期発見できる。
テーブル設計者やシステム設計者と、システム開発者の間にはどうしても認識のずれが発生してしまう。個々の開発者にテストデータを作成させると、ずれた認識のまま作成してしまうため、プロジェクトの後期にならないと不正な実装を発見できなくなってしまう。本番に近いデータを用意しておくことで、このような不安定要素を軽減することができる。

単体テスト導入の第一歩

単体テストの必要性が説かれてからずいぶん時が過ぎたが、今だに導入していないプロジェクトがたくさんある(少なくとも日本では。)
xUnit系のフレームワークも、いろいろな目的に利用できるものが簡単に手に入る。単体テストによる品質の向上や開発作業の効率化などの文献や統計もたくさん出ている。これを利用しない手はないと思うんだけどなぁ。
実は、詳細でカバレッジ率の高いテストコードを書かずに簡単なテストコードを記述するだけでも、品質と安定性は格段に向上することになる。その実例を以下に示す。

サンプルコード

StringUtility
public class StringUtility{
    public static boolean isAvailableFileName(String param){
        try{
            return param.endsWith(".zip");
        }catch(NullPointerException e){
            return false;
        }
    }
}

Client
import java.io.File;
public class Client {
    public File createFile(String filename){
        if(!StringUtility.isAvailableFileName(filename)){
            return null;
        }
        return new File(filename);
    }

    public static void main(String[] args) {
        Client client = new Client();
        File file = client.createFile(null);
    }
}

上記の単純なコードを実際に実行してみる。何の問題もなく終了する。

ここで、StringUtilityクラスのisAvailableFileNameメソッドに仕様変更を行う。現在はパラメータがnullの場合、falseを返却している。この例外処理を除外し、nullの場合はStringクラスによって投げられるNullPointerExceptionをそのまま呼び出し元に投げるようにする。

変更後のisAvailableFileNameメソッド
public static boolean isAvailableFileName(String param){
    return param.endsWith(".zip");
}

これにより、StringUtils.isAvailableFileNameメソッドを利用しているすべてのクラスが影響を受けることになる。nullを渡した場合、例外によりプログラムが強制終了してしまう。
上記のような例で一番重要な問題は、「実際にアプリケーションを動かさないと問題が表面化しない。」ということである。
Clientクラスの(従来のチェックシートのような)単体テストが終了しているなら、実際に運用が開始されるか、開発中に偶然発見されるまで問題が表面化しない恐れがある。

さて、実は上記のような問題は簡単に回避することが出来る。単体テストを好きなときに手軽に実行できるように、自動化するのである。そして実際に複雑なテストメソッドを利用しなくても、上記のような異常系の処理をすぐに発見することが可能となる。これは決して大げさではない。

テストを書く
public class LogicTest extends TestCase{
    public void testSetData() throws Exception{
        Logic logic = new Logic();
        logic.setData(new Data("office1", "00-0000-0000"));
    }
}

上記の例では定義されたクラスに対してテストクラスを定義しているが、比較メソッド(assertEquals等)を利用していない。しかし、前述の問題に対応するためには、これで十分なのである。
実際にStringUtilsクラスに変更を行った後にこのテストを実行すると、テストは失敗するため、問題をすぐに発見することができる。詳細な比較メソッドを利用しなくても、異常系の例外を表面化することができるのである。

これだけの作業で得ることができるメリットをあげてみる。
  • 問題の早期発見
    仕様変更により発生する問題を、早期に、コストをかけずに発見することができる。
  • 影響範囲の認知
    テストの失敗部分が目に見える。これは仕様変更によって影響を受ける部分を見ることが出来ることを意味する。
  • 修正の見通しが立つ
    仕様変更によって通らなくなったすべてのテストが通るようになった時が、修正の終了時点である。

単にメソッド呼び出しのテストコードを書くだけで、これだけのメリットがある。明らかにシステムの品質と安定性は向上し、無駄な時間は減少する。

Mockオブジェクトを利用したテストを行うための設計

DIパターン

DIを利用して、単体テスト時にMockオブジェクトをインジェクションする。

クラスからオブジェクト生成を意識することがないので、設計における制約が減少する。

ServiceLocatorパターン

ベストプラクティスはDIパターンの利用だが、DIの利用できないプロジェクトが多いのでJ2EEパターンのServiceLocatorを利用する。

一例として、サービス検索のためのキーと実装クラスのマッピングを外部ファイルに定義しておき、テスト時にMockが定義されているファイルと置き換えて利用する。

FactoryMethodパターン

テスト時にFactoryクラスをテスト用のものに置き換えるか、コードを変更することでMockオブジェクトを生成するように実装する。

手軽に実装できるが、強い依存関係が現れるため、大規模システムには向いていない。

テスト作成の法則

テストを作成するときには次のことを意識すること。
  • 単純に作る。
  • わかりやすいメソッド名をつける。
  • メソッドの粒度を統一する。
  • テスト内にロジックを記述しない。

単純に作る。

テストの構造を単純にすること。可読性の高いコードを記述することを心がける。

わかりやすいメソッド名をつける。

かなり重要。

メソッドの粒度を統一する。

テストメソッドの単位には「適当」「シナリオ単位」「メソッド単位」など、いろいろな粒度を用いることができるが、できるかぎり全体で統一する。ただし、「このプロジェクトにおけるテストはすべてシナリオ単位で書いてください。」という方法ではなく、「ユーティリティー系のクラスはメソッド単位。」「画面フローのテストはシナリオ単位。」など、それぞれの特徴に合わせるのがよい。

テスト内にロジックを記述しない。

テスト内にロジックを記述した場合、保守性が著しく低下することになる。ビジネスロジックの修正に伴い、テスト内のロジック修正に時間を割くようでは本末転倒である。

テストから仕様を理解する

単体テストを、仕様をすばやく理解するためのドキュメントとして利用する。
これにより、永続化処理やユーティリティーの動作などを、複雑なドキュメントを参照するよりもすばやく得ることができる。

クラスの設計書を開いて「nullが渡された場合、falseが返ります。」というような記述を探すよりも、そのクラスのテストコードから以下の一行を参照するほうが早い。
assertEquals(UtilClass.method(null), false)
仕様を詳細に理解することができる。また、あいまいさが含まれないため、誤解が生じる可能性が極めて低い。

これを踏まえて、テストの作成時にはドキュメントとしても利用されることを意識して、クリアなコードを記述すること。

Advertisement

ショートカット

634
634ブログ
このカテゴリのトップページに戻る
Incubator(Pukiwiki)
634ラボ
   UIコレクションギャラリー
   ZO-3ジェネレーター

サイト検索


Y!ログール