トピック

概論 ページの先頭へ

コンポーネントをテストするには、そのインターフェイスに入力情報を送り、コンポーネントがそれらを処理するのを待って、その結果を確認します。その処理の過程で、コンポーネントが他のコンポーネントに入力情報を送り、その結果を使用することによって他のコンポーネントを使用する可能性は非常に高いです。

図 1: 実装したコンポーネントのテスト

これらの他のコンポーネントは、テストの際に次のような問題を招く可能性があります。

  1. それらのコンポーネントがまだ実装されていない可能性があります。
  2. テストの作業を妨げたり、テスト障害が自分のコンポーネントによって引き起こされたのではないことを突き止めるのに多くの時間を費やさせる不具合を持っている可能性があります。
  3. 必要な場合にテストを実行するのを困難にする可能性があります。コンポーネントが商用データベースである場合、会社は全員のための十分なフローティング ライセンスを持っていない可能性があります。コンポーネントのいずれかが、別個のラボでスケジュールされた時間にのみ利用できるハードウェアである場合もあります。
  4. テストが十分に実行されないほどテストを遅くさせる可能性があります。たとえば、データベースの初期化が 1 つのテストにつき 5 分かかることもありえます。
  5. コンポーネントに特定の結果を生み出させるのが難しいこともあります。たとえば、「ディスク フル」エラーを処理するためにディスクに書き込む各メソッドが必要になる場合もあります。そのメソッドが呼び出される瞬間でディスクがいっぱいになるのをどのように確認しますか。

これらの問題を回避するために、スタブコンポーネント (モック オブジェクトとも呼ばれる) を使用することができます。スタブ コンポーネントは、少なくともコンポーネントがテストに応答している間にスタブ コンポーネントに送る値に関しては、本当のコンポーネントのように振舞います。それ以上の機能を発揮する場合もあります。スタブ コンポーネントは、ほとんどまたはすべてのコンポーネントの振る舞いを忠実に模倣しようとする多目的のエミュレータになることもできます。たとえば、通常、ハードウェアのためのソフトウェア エミュレータを構築するのは良い戦略です。それらのエミュレータは、遅くなるだけでハードウェアと同様に振る舞います。それらのエミュレータは、より良いデバッグをサポートし、より多くのコピーが利用でき、ハードウェアが完成する前に使用することができるので役に立ちます。

図 2: 依存しているコンポーネントからスタブ アウトして実装されたコンポーネントのテスト

スタブには 2 つの不利な点があります。

  1. 構築するのに費用がかかる可能性があります。(それは特にエミュレータに当てはまります。) それら自体でソフトウェアであるので、保守する必要があります。
  2. エラーを隠す可能性があります。たとえば、コンポーネントは三角関数を使用するが、ライブラリはまだ利用できないものと想定します。3 つのテスト ケースで、3 つの角度 (10 度、45 度、90 度) の正弦を求めます。電卓を使用して正しい値を求めた後、それぞれ 0.173648178、0.707106781、1.0 を返す正弦のためのスタブを作成します。コンポーネントを真の三角法のライブラリと統合するまでは何の問題もありませんが、その三角法のライブラリの正弦関数は、ラジアンの引数を取るので、-0.544021111、0.850903525、0.893996664 を返します。それは後で発見されるコードの問題であり、より多くの作業を要します。

スタブとソフトウェア設計の実践原則 ページの先頭へ

実際のコンポーネントがまだ利用できなかったためにスタブが作成されなかった場合を除き、スタブは過去の導入を維持すると考える必要があります。それらがサポートしているテストは、製品の保守時に重要になる可能性があります。したがって、スタブは、使い捨てのコードよりも高い標準に合わせて記述される必要があります。スタブは製品コードの標準を満たす必要はありませんが -- たとえば、大部分はそれら独自のテスト セットは必要ありません -- 後の開発者は、製品への変更のコンポーネントとしてスタブを保守する必要があります。その保守が困難すぎる場合、スタブは破棄されるので、それらへの投資は失われます。

特に、それらが維持されることになっている場合、スタブはコンポーネントの設計を変更します。たとえば、コンポーネントはデータベースを使用してキー / 値ペアを永続的に格納すると想定します。以下の 2 つの設計シナリオについて考えてみます。

シナリオ 1: テストと通常の使用にデータベースを使用する。コンポーネントからデータベースの存在を隠す必要はありません。次のようにデータベースの名前を使ってコンポーネントを初期化することができます。

    public Component(String databaseURL) {
        try {
            databaseConnection =
                DriverManager.getConnection(databaseURL);
            ...
        } catch (SQLException e) {...}
    }

値を読み取る、または書き込むそれぞれの位置に SQL 文を作成する必要はないにしても、SQL を含むメソッドは確かにいくつかあります。たとえば、値が必要なコンポーネント コードによって、このコンポーネント メソッドが呼び出される可能性があります。

    public String get(String key) {
        try {
            Statement stmt =
              databaseConnection.createStatement();
            ResultSet rs = stmt.executeQuery(
              "SELECT value FROM Table1 WHERE key=" + key);
            ...
        } catch (SQLException e) {...}
    }

シナリオ 2: テストの際にデータベースをスタブに置き換える。本当のデータベースに対してまたはスタブに対して実行しているかどうかに関係なく、コンポーネント コードは同じに見える必要があります。したがって、抽象インターフェイスのメソッドを使用するためにコード化される必要があります。

    interface KeyValuePairs {
        String get(String key);
        void put(String key, String value);
    }

テストでは、ハッシュ テーブルのような何か簡単なものを使用して KeyValuePairs を実装します。

    class FakeDatabase implements KeyValuePairs  {
        Hashtable table = new Hashtable();

        public String get(String key) {
            return (String) table.get(key);
        }

        public void put(String key, String value) {
            table.put(key, value);
        }
    }

テストされない場合、コンポーネントは、KeyValuePairs インターフェイスへの呼び出しを SQL 文に変換したアダプタ オブジェクトを使用します。

    class DatabaseAdapter implements KeyValuePairs {
        private Connection databaseConnection;
        
        public DatabaseAdapter(String databaseURL) {
            try {
                databaseConnection =
                    DriverManager.getConnection(databaseURL);
                ...
            } catch (SQLException e) {...}
        }

        public String get(String key) {
            try {
                Statement stmt =
                  databaseConnection.createStatement();
                ResultSet rs = stmt.executeQuery(
                  "SELECT value FROM Table1 WHERE key=" + key);
                ...
            } catch (SQLException e) {...}
        }

        public void put(String key, String value) {
            ...
        }
    }

コンポーネントは、テストと他のクライアントの両方に対して 1 つのコンストラクタを使用することができます。そのコンストラクタは、KeyValuePairs を実装するオブジェクトを取ります。そのインターフェイスをテストにだけ提供することもでき、その場合はコンポーネントの通常のクライアントはデータベースの名前で渡す必要があります。

    class Component {
        public Component(String databaseURL) {
            this.valueStash = new DatabaseAdapter(databaseURL);
        }

        // For testing.
        protected Component(KeyValuePairs valueStash) {
            this.valueStash = valueStash;
        }
    }

したがって、クライアント プログラマの観点から、2 つの設計シナリオは同じ API をもたらしますが、1 つはより簡単にテストできます。(一部のテストは本当のデータベースを使用する可能性があり、一部はスタブ データベースを使用する可能性があることに注意してください。)

詳細情報 ページの先頭へ

スタブに関連する詳細情報については、以下のものを参照してください。



Rational Unified Process   2003.06.15