生成テスト

Pex によるレガシ コードの自動単体テスト

Nikhil Sachdeva

サンプル コードのダウンロード

前職はコンサルタントでした。私のクライアントの 1 社だった大手銀行が、融資取組プロセスの自動化を望んでいました。この銀行には、既に、Windows ベースのアプリケーションを含むシステム、専用のバックエンド、および同じくソリューションの要であるメインフレーム システムが導入されていました。私の仕事は、既存のシステムと、経理部門用に開発中だった一連のアプリケーションを統合することでした。

このクライアントでは、メインフレームと通信する Web サービスを構築していました。最初、この仕事は非常に簡単に思えました。必要なことは、このサービスにフックして、情報を取得し、その情報を新しい経理アプリケーションに渡すだけだと思えたのです。ところが、とてもそんな簡単な話ではありませんでした。

実装中に、新しいシステムには "融資を行う主体" 属性が必要でしたが、Web サービスの GetLoanDetails メソッドではその情報が返されないことがわかりました。また、このサービスは、既に退職した開発者が何年も前に作成したものでした。このサービスには多くの層があり、何かが機能しなくなることを恐れて、銀行ではこのサービスを変更することなく使用していました。

この Web サービスは、レガシ コードです。

最終的に、軽量のサービス ラッパーを作成して、新しいシステムからこれを呼び出して新しい情報を取得すると同時に、古いサービスも引き続き使用できるようにしました。一貫性があり、テスト可能な設計にしたがっていたなら、既存のサービスを変更することはすいぶんと簡単だったでしょう。

コードを最新の状態に保つ

レガシ コードは、以前開発されたもので、現在も使用されていますが、メンテナンスと変更が困難なコードです。このようなシステムを維持する理由は、基本的に、同様の新しいシステムの構築にかかるコストと時間によります。しかし、現在のコーディング作業が将来的に与える影響については、見落とされていることがあります。

時間の経過とともに、コードが現状にそぐわなくなるというのが現実です。この原因としては、要件の変更、十分に考えられていない設計、アンチパターンの適用、適切なテストの未実施などが考えられます。最終的に、メンテナンスが難しく、変更しづらいコードになります。

コードが現状にそぐわなくなるのを防ぐ方法はいくつもありますが、最も有効な方法の 1 つは、テスト可能なコードを作成し、そのコード用に十分な単体テストを用意することです。単体テストは、継続的にシステムにカバーされていないパスないかを探り、厄介なエラーを特定し、サブシステムに導入された変更がシステム全体に好影響、悪影響を与えていないかどうかを示す指標を提供する媒体の役目を果たします。単体テストを実施することで、開発者はコードの変更によって機能低下が起きないことを確信できます。

そうは言っても、適切な単体テスト スイートを作成し、メンテナンスすること自体が難しい課題です。テスト対象のコードよりも、テスト スイートのコードを多く作成しなければならなくなる可能性があります。また別の課題は、コード内の依存関係です。ソリューションが複雑になるほど、クラス間に見つかる依存関係も多くなると思われます。モックやスタブは、このような依存関係を取り除き、コードを分離してテストする手法として認められていますが、有効な単体テストを作成するには、開発者にさらに多くの知識と経験が必要です。

Pex による救済

Pex (research.microsoft.com/projects/pex/、英語) は、Microsoft Research によって開発されたツールで、限られた数の限られたパスを実行するために必要なテスト入力の最小セットを自動的かつ体系的に生成するツールです。Pex は、コードとアサーションのカバレッジが高い、小規模テスト スイートを自動的に生成します。

Pex は、テストが対象とするメソッドの入出力値を検出します。検出された値は、コード カバレッジの高い、小さなテスト スイートとして保存できます。Pex は体系的な分析を実行して、境界条件、例外、およびアサーション エラーを探し、見つかったエラーは直ちにデバッグできます。また、Pex では、テストのメンテナンス コストを削減できる単体テストの拡張形であるパラメーター化された単体テスト (PUT) も作成できるほか、動的な記号的実行を使用して、テスト対象のコードを調べ、ほとんどの実行分岐をカバーするテスト スイートを作成します。

PUT は、単にパラメーターを受け取り、テスト対象のコードを呼び出して、アサーションを定義するメソッドです。サンプルの PUT を次に示します。

void AddItem(List<int> list, int item) {

  list.Add(item);

  Assert.True(list[list.Count - 1] == item);

}

PUT の概念は、データ ドリブン テスト (DDT) と呼ばれるさらに広い概念の用語から派生したものです。DDT は、テストを繰り返し実行できるようにするために、従来の単体テストで長年使用されてきました。従来の単体テストは閉じた性質であるため、従来の単体テストに入力値を渡すには、XML ファイル、スプレッドシート、データベースなどの外部ソースを使用するしかありませんでした。DDT の手法は十分に役立ちましたが、外部データ ファイルのメンテナンスと変更に伴う追加のオーバーヘッドが発生するほか、入力の内容は、開発者が持つシステムについての知識によって決まります。

Pex では外部ソースを利用せず、対応する PUT に値を渡すことで、テスト メソッドに入力を提供します。PUT はオープンなメソッドであるため、任意の数の入力値を受け取るように調整できます。また、Pex は PUT 用にランダム値を生成しません。Pet では、テスト対象のメソッドのイントロスペクションを実行して、境界条件、許容可能な型の値、最先端の制約ソルバーである Z3 (research.microsoft.com/en-us/um/redmond/projects/z3/、英語) などの要素を基に意味のある値を生成します。これにより、テスト対象メソッドのすべての関連パスが、確実にカバーされます。

Pex の便利な点は、PUT から従来の単体テストを生成できることです。生成された単体テストは、変更せずに、Visual Studio の MSTest などの単体テスト フレームワークから直接実行できます。Pex には、NUnit や xUnit.NET などのフレームワーク用の単体テストを生成するための拡張機能も用意されています。また、独自にカスタムの拡張機能を作成することもできます。Pex によって生成された従来の単体テストの例を次に示します。

[TestMethod]

[PexGeneratedBy(typeof(TestClass))]

void AddItem01() {

  AddItem(new List<int>(), 0);

}

動的な記号的実行は、探索的テストに対して Pex が出した答えです。Pex ではプログラムの動作を理解するために、この技法を使用してコードを複数回実行します。コントロールとデータ フローを監視し、テスト入力に使用する制約システムを構築します。

Pex による単体テスト

最初の作業は、テスト対象のコード用の PUT を作成することです。PUT は開発者が手動で生成することも、Visual Studio の Pex 用アドインを使用して生成することもできます。パラメーターの変更、Pec ファクトリとスタブの追加、モックとの統合、アサーションの追加などによって、PUT を調整できます。Pex ファクトリとスタブについては、後ほど説明します。

現在、Visual Studio の Pex 用アドインでは、C# でしか PUT が作成されませんが、テスト対象コードの言語はどの .NET 言語でもかまいません。

PUT が設定できたら、次は Pex 探索を実行します。Pex が本領を発揮するのは、この部分です。Pex は PUT を分析して、テストされる内容を特定します。次に、各分岐を通過して、入力値の候補を評価することで、テスト対象のコードの調査を開始します。Pex は、テスト対象のコードを繰り返し実行します。1 回の実行が終わると、前にカバーされなかった分岐を選び、その分岐に到達するための制約システム (テスト入力に対する述語) を構築し、制約ソルバーを使用して新しいテスト入力を必要に応じて決定します。そして、新しい入力を使用してテストが再び実行され、このプロセスが繰り返されます。

実行ごとに、Pex が新しいコードを発見し、実装を詳細に検証する可能性があります。このようにして、Pex はコードの動作を探索します。

Pex はコードの探索中に、Pex が実行できるすべての分岐をカバーするテストを含む単体テスト スイートを生成します。これらのテストは、Visual Studio のテスト エディターで実行できる標準の単体テストです。一部のセクションのカバレッジが低かった場合は、リファクタリングを行い、同じサイクルを再度適用して、コード カバレッジを高めてより包括的なテスト スイートを作成できるように、コードを書き換えることを検討してください。

Pex を使用すると、レガシ コードを扱う場合の労力と時間を節約できます。Pex は自動的に異なる分岐とコード パスを探索するため、開発者がコード ベース全体の詳細をすべて把握しておく必要がありません。もう 1 つのメリットは、開発者が PUT レベルで作業することです。多くの場合、PUT を作成することは、対象の機能のすべてのテスト ケースの候補を検討するのではなく、問題のシナリオに集中できるため、閉じられた単体テストを作成するよりもはるかに簡単です。

Pex を使用する

ここでは、レガシ コードに対して Pex を使用して、そのコードをよりメンテナンスしやすく、簡単にテストできるコードに変更する例を見ていきましょう。

バスケットボールと野球ボールの大手メーカーである Fabrikam は、ユーザーが製品を参照して注文できるオンライン ポータルを運営しています。在庫の詳細情報は、カスタムのデータ ストアに保存されています。このデータ ストアには、このストアへの接続を提供する Warehouse コンポーネントや、HasInventory、Remove などの操作によってアクセスします。Order コンポーネントは、ユーザーが指定した製品と個数を基に注文を処理する Fill メソッドを提供します。

Order コンポーネントと WareHouse コンポーネントは、密結合されています。これらのコンポーネントは何年も前に開発されていて、現在の社員でシステムを完全に理解している人はいません。開発中に単体テストは作成されておらず、おそらくそのために、これらのコンポーネントは非常に不安定です。現在の設計を図 1 に示します。

A Legacy Order Fulfillment System

図 1 レガシ受注処理システム

Order クラスの Fill メソッドを次に示します。

public class Order {

  public bool Fill(Product product, int quantity) {

    // Check if WareHouse has any inventory

    Warehouse wareHouse = new Warehouse();

    if (wareHouse.HasInventory(product, quantity)) {

      // Subtract the quantity from the product in the warehouse

      wareHouse.Remove(product, quantity);

      return true;

    }



    return false;

  }

}

ここで注意すべき重要な点がいくつかあります。まず、Order と Warehouse は密結合されています。クラスは、拡張性の低い、モックやスタブ フレームワークの使用を困難にする実装に依存しています。単体テストがないため、変更を行った場合、機能低下を引き起こし、システムが不安定になる可能性があります。

Warehouse コンポーネントはかなり以前に作成されているため、現在の開発チームには、このコンポーネントの変更方法や変更の影響がまったくわかりません。さらに事態を複雑にしているのは、Order は変更なしには Warehouse の他の実装とは連携できないことです。

コードをリファクタリングし、さらに Pex を使用して単体テストを生成してみましょう。ここでは、Warehouse オブジェクトと Order オブジェクトをリファクタリングし、次に Order クラスの Fill メソッドの単体テストを作成します。

レガシ コードのリファクタリングは、明らかに難しい作業です。このような場合のアプローチとしては、少なくともコードをテスト可能にして、十分な単体テストを生成できるようにすることが考えられます。ここでは、コードをテスト可能にするための本当に最小限のパターンのみを適用します。

ここでの最初の問題は、Order が Warehouse のある特定の実装を使用することです。これは、その Warehouse の実装を Order から切り離すことを難しくしています。コードを少し変更して、より柔軟でテスト可能なコードにしてみましょう。

まず、IWareHouse というインターフェイスを作成し、Warehouse オブジェクトを変更してこのインターフェイスを実装します。新しい Warehouse には、必ずこのインターフェイスを実装することを求めます。

Order は Warehouse に直接依存しているため、Order と Warehouse は密結合の関係にあります。ここでは、依存関係の挿入 (DI) を使用して、このクラスをオープンにし、動作を拡張できるようにします。このアプローチにより、IWareHouse インスタンスが実行時に Order に渡されます。新しい設計を図 2 に示します。

Revised System with IWareHouse

図 2 IWareHouse を実装する変更後のシステム

新しい Order クラスを図 3 に示します。

図 3 変更後の Order クラス

public class Order {

  readonly IWareHouse orderWareHouse;



  // Use constructor injection to provide a wareHouse object

  public Order(IWareHouse wareHouse) {

    this.orderWareHouse = wareHouse;

  }



  public bool Fill(Product product, int quantity) {

    // Check if WareHouse has any inventory

    if (this.orderWareHouse.HasInventory(product, quantity)) {

      // Update the quantity for the product

      this.orderWareHouse.Remove(product, quantity);

      return true;

    }

    return false;

  }

}

パラメーター化された単体テスト (PUT) を作成する

次に、Pex を使用して、リファクタリングしたコードのテストを生成しましょう。Pex には、PUT を簡単に生成するための Visual Studio アドインがあります。PUT を生成する必要があるプロジェクト、クラス、またはメソッドを右クリックし、[Pex] (Pex) をクリックして [Create Parameterized Unit Test Stubs] (パラメーター化された単体テスト スタブの作成) をクリックします。ここでは、まず、Order クラスの Fill メソッドを選択しました。

Pex では、既存の単体テスト プロジェクトを選択することも、新しい単体テストを作成することもできます。また、メソッドや型名を基に、テストをフィルター処理することもできます (図 4 参照)。

Setting up a New Pex Project

図 4 新しい Pex プロジェクトの設定

Pex により、Fill メソッドに対して次の PUT が生成されます。

[PexClass(typeof(Order))]

[TestClass]

public partial class OrderTest {

  [PexMethod]

  public bool Fill([PexAssumeUnderTest] Order target, 

    Product product, int quantity) {



    // Create product factory for Product

    bool result = target.Fill(product, quantity);

    return result;

  }

}

OrderTest は単なる通常の TestClass ではありません。OrderTest には PexClass 属性の注釈が付けられていて、Pex によって作成されたことが示されています。ご想像のとおり、現在、標準の Visual Studio 単体テストには、TestMethod はありません。代わりに、PexMethod を利用できます。このメソッドが PUT です。後で Pex によりテスト対象のコードの探索を行うときに、PexMethod は、TestMethod 属性の注釈が付けられた標準の単体テストを含む別個の部分クラスを作成します。生成されたこれらのテストは、Visual Studio のテスト エディターから利用できます。

Fill メソッドの PUT には、3 つのパラメーターがあることに注意してください。

[PexAssumeUnderTest] Order target

これは、テスト対象のクラスそのものです。PexAssumeUnderTest 属性は、確実に指定された型の null ではない値のみを渡すように Pex に指示します。

Product product

これは Product 基本クラスです。Pex は、この Product クラスのインスタンスを自動的に作成します。より細かく制御する場合は、Pex にファクトリ メソッドを提供できます。Pex は渡されたファクトリを使用して、複雑なクラスのインスタンスを作成します。

int quantity

Pex は、テスト対象のメソッドを基に、個数の値を提供します。Pex では、でたらめな値ではなく、テストにとって意味のある値の挿入が試みられます。

Pex ファクトリ

前述のとおり、Pex は制約ソルバーを使用して、パラメーターに使用する新しいテスト入力を決定します。入力には、標準の .NET 型も、カスタムのビジネス エンティティも使用できます。探索中に、Pex は実際には 3 種類のインスタンスを作成し、テスト対象のプログラムがテストに適したさまざまな方法で動作できるようにします。クラスが表示されて、クラスに表示可能な既定のコンストラクターがある場合、Pex はそのクラスのインスタンスを作成できます。すべてのフィールドが表示される場合は、それらのフィールドの値も生成できます。ただし、フィールドがプロパティにカプセル化されていたり、外部に公開されていない場合は、オブジェクトを作成して、コード カバレッジを高める必要があります。

Pex には、必要なオブジェクトを作成して Pex の探索にリンクするための 2 種類のフックが用意されています。異なるオブジェクトの状態を Pex が探索できるように、ユーザーは、複雑なオブジェクト用にファクトリを用意することもできます。それには、Pex Factory メソッドを使用します。このようなファクトリから作成できる型は、探索可能型と呼ばれます。ここでは、このアプローチを使用します。

もう 1 つのアプローチは、オブジェクトのプライベート フィールドのインバリアントを定義して、Pex が異なるオブジェクト状態を直接作成できるようにする方法です。

サンプルのシナリオに戻ると、パラメーター化されたテストを生成するために Pex 探索を実行した場合、[Pex Exploration Results] (Pex 探索の結果) ウィンドウに "2 Object Creations" (2 つのオブジェクトが作成されました) というメッセージが表示されます。これは、エラーではありません。探索中に Pex は複雑なクラス (この場合は Order) を検出したので、そのクラス用に既定のファクトリを作成しました。このファクトリは、Pex がプログラムの動作をよりよく理解するために必要です。

Pex が作成する既定のファクトリは、必要なクラスの基本実装です。このファクトリを調整して、独自のカスタム実装を用意できます。[Accept/Edit Factory] (ファクトリの承認/編集) をクリックして、コードをプロジェクトに挿入します (図 5 参照)。または、PexFactoryMethod 属性の注釈を付けた静的メソッドを含む静的クラスを作成できます。探索中に Pex は、テスト プロジェクトを検索して、この属性が設定されたメソッドを持つ静的クラスの存在を確認し、適宜このようなクラスを使用します。

Creating the Default Factory

図 5 既定のファクトリの作成

OrderFactory のコードを次に示します。

public static partial class OrderFactory {

  [PexFactoryMethod(typeof(Order))]

  public static Order Create(IWareHouse wareHouseIWareHouse) {

        

    Order order = new Order(wareHouseIWareHouse);

    return order;

  }

}

他のアセンブリにファクトリ メソッドを作成する場合は、たとえば、アセンブリ レベルの PexExplorableFromFactoriesFromType 属性または PexExplorableFromFactoriesFromAssembly 属性を使用して、それらのファクトリ メソッドを使用するように宣言によって Pex に指示できます。

[assembly: PexExplorableFromFactoriesFromType(

  typeof(MyTypeInAnotherAssemblyContainingFactories))]

Pex によってごく少数のテストしか作成されない、または作成する予定のオブジェクトに対して NullReferenceException をスローするだけで適切なテストが作成されない場合は、Pex にカスタム ファクトリが必要な可能性が高いことを示しています。または、Pex に付属しているヒューリスティックのセットを使用して、多くの場合に利用できるオブジェクト ファクトリを作成できます。

Pex Stubs フレームワーク

ソフトウェア開発では、テスト スタブの概念は、複雑になる可能性があるコンポーネントと置換できるダミー実装を表します。この実装は、テストを容易にするために使用されます。スタブの概念は単純ですが、ダミー実装の作成とメンテナンスに役立つほとんどの既存のフレームワークは、実際には非常に複雑です。そこで、Pex チームは、その名も Stubs という新しい軽量のフレームワークを開発しました。Stubs は、.NET インターフェイスおよび非シール クラス用のスタブ型を生成します。

このフレームワークでは、型 T のスタブが T の各抽象メンバーの既定の実装と、各メンバーのカスタム実装を動的に指定するメカニズムを提供します (必要に応じて、非抽象の仮想メンバーに対してスタブを生成することもできます)。スタブの方は C# コードとして生成されます。フレームワークは、デリゲートのみを利用して、動的にスタブ メンバーの動作を指定します。Stubs は .NET Framework 2.0 以上をサポートし、Visual Studio 2008 以上と統合します。

今回のサンプル シナリオでは、Order 型が Warehouse オブジェクトに依存しています。ただし、コードをリファクタリングして、依存関係の挿入を実装し、Warehouse に Order 型以外からアクセスできるようにしました。これが、スタブを作成するときに役に立ちます。

スタブの作成はとても簡単です。必要なのは、.stubx ファイルのみです。Pex からテスト プロジェクトを作成している場合は、既にこのファイルは作成されています。このファイルがなければ、Visual Studio から作成できます。テスト プロジェクトを右クリックし、[新しい項目の追加] をクリックします。Stubs テンプレートを利用できます (図 6 参照)。

Creating a New Stub

図 6 スタブの新規作成

作成されたファイルが、標準の XML ファイルとして Visual Studio に表示されます。Assembly 要素に、スタブを作成する必要があるアセンブリの名前を指定し、.stubx ファイルを保存します。

<Stubs xmlns="https://schemas.microsoft.com/stubs/2008/">

  <Assembly Name="FabrikamSports" />

</Stubs>

Pex によって、アセンブリのすべての型に対して、必要なスタブ メソッドが自動的に作成されます。

生成されたスタブ メソッドには、対応するデリゲート フィールドがあり、これらのフィールドがスタブ実装へのフックを提供します。既定では、Pex はデリゲートの実装を提供します。また、ラムダ式を指定して動作をデリゲートにアタッチするか、PexChoose 型を使用して Pex によって自動的にメソッドの値を生成することもできます。

たとえば、HasInventory メソッド用の選択肢を提供するには、次のようなコードを利用できます。

var wareHouse = new SIWareHouse() {

  HasInventoryProductInt32 = (p, q) => {

    Assert.IsNotNull(p);

    Assert.IsTrue(q > 0);

    return products.GetItem(p) >= q;

  }

};

実際、Pex によって作成されたテスト プロジェクトには、次のアセンブリ レベルの属性が含まれているため、Pex を使用する場合、既に PexChoose を使用することはスタブの既定の動作になっています。

[assembly: PexChooseAsStubFallbackBehavior]

SIWareHouse 型は Pex Stubs フレームワークによって生成されます。SIWareHouse 型は、IWareHouse インターフェイスを実装します。SIWareHouse 用に Pex によって生成されたコードを、さらに詳しく見ていきましょう。.stubx ファイルのソース コードは、<Stubsx ファイル名>.designer.cs という名前の部分クラスに作成されます (図 7 参照)。

図 7 Pex により生成された IWareHouse スタブ

/// <summary>Stub of method System.Boolean 

/// FabrikamSports.IWareHouse.HasInventory(

/// FabrikamSports.Product product, System.Int32 quantity)

/// </summary>

[System.Diagnostics.DebuggerHidden]

bool FabrikamSports.IWareHouse.HasInventory(

  FabrikamSports.Product product, int quantity) {

    

  StubDelegates.Func<FabrikamSports.Product, int, bool> sh

    = this.HasInventory;

  if (sh != (StubDelegates.Func<FabrikamSports.Product, 

    int, bool>)null)

    return sh.Invoke(product, quantity);

  else {

    var stub = base.FallbackBehavior;

    return stub.Result<FabrikamSports.Stubs.SIWareHouse, 

      bool>(this);

  }

}



/// <summary>Stub of method System.Boolean 

/// FabrikamSports.IWareHouse.HasInventory(

/// FabrikamSports.Product product, System.Int32 quantity)

/// </summary>

public StubDelegates.Func<FabrikamSports.Product, int, bool> HasInventory;

Stubs によって HasInventory メソッド用のパブリック デリゲート フィールドが作成され、これが HasInventory 実装から呼び出されています。実装がない場合は、Pex は FallBackBehaviour.Result メソッドを呼び出します。このメソッドは、[assembly: PexChooseAsStubFallbackBehavior] が存在する場合は PexChoose を使用し、そうでない場合は StubNotImplementedException をスローします。

IWareHouse のスタブ実装を使用するために、PUT を少し調整します。既に Order クラスを変更して、このクラスのコンストラクターで IWareHouse 実装を利用できるようにしています。今度は、SIWareHouse インスタンスを作成し、これを Order クラスに渡して、Order クラスが IWareHouse メソッドのカスタム実装を使用できるようにします。変更後のスクリプトを次に示します。

[PexMethod]

public bool Fill(Product product, int quantity) {

  // Customize the default implementation of SIWareHouse

  var wareHouse = new SIWareHouse() {

    HasInventoryProductInt32 = (p, q) => 

    PexChoose.FromCall(this).ChooseValue<bool>(

    "return value")

  };



  var target = new Order(wareHouse);

  // act

  bool result = target.Fill(product, quantity);

  return result;

}

実際には、Stubs によって自動的にスタブ メソッドの既定の実装が提供されるので、開発者は単純に何の変更もせずに PUT を実行できます。

パラメーター化されたモデル

より細かくスタブ実装を制御できるように、Pex ではパラメーター化されたモデルと呼ばれる概念をサポートしています。これは、動作が 1 つに決められていないスタブを作成する手段の 1 つです。Pex がこの概念により実現する抽象化のおかげで、開発者は実装のバリエーションを気に掛ける必要がありません。Pex は、メソッドの各戻り値がテスト対象のコードによってどのように使用されるかを基に、それらの戻り値を探索します。パラメーター化されたモデルは、開発者がスタブの処理方法を完全に制御できるようにすると同時に、Pex による入力パラメーターのバリアント値の評価も許可する強力な機能です。

IWareHouse のパラメーター化されたモデルのコード例を 図 8 に示します。

図 8 IWareHouse のパラメーター化されたモデル

public sealed class PWareHouse : IWareHouse {

  PexChosenIndexedValue<Product, int> products;



  public PWareHouse() {

    this.products = 

      new PexChosenIndexedValue<Product, int>(

      this, "Products", quantity => quantity >= 0);

  }



  public bool HasInventory(Product product, int quantity) {

    int availableQuantity = this.products.GetItem(product);

    return quantity - availableQuantity > 0;

  }



  public void Remove(Product product, int quantity) {

    int availableQuantity = 

    this.products.GetItem(product); 

    this.products.SetItem(product, 

      availableQuantity - quantity);

  }

}

基本的に、ここでは IWareHouse 用に独自のスタブ実装を作成しましたが、Quantity と Product の値は提供していないことに注意してください。これらの値については、Pex によって生成しています。PexChosenIndexedValue が自動的にオブジェクトの値を提供するので、可変のパラメーター値を使用するスタブ実装を 1 つ用意するだけで十分です。

手間を省くため、ここでは Pex によって IWareHouse 型の HasInventory 実装を用意します。また、前に作成した OrderFactory クラスにコードを追加します。Order のインスタンスが Pex によって作成されるたびに、Order のインスタンスはスタブ化された Warehouse のインスタンスを使用します。

モル (Mole)

ここまで、コードをリファクタリングしてテスト可能にし、Pex を使用して単体テストを生成するという 2 つの原則について説明してきました。このアプローチでは、コードを整理でき、最終的にはよりメンテナンスしやすいソフトウェアが得られます。ただし、レガシ コードのリファクタリングは、それ自体が大変な作業です。現在のソース コードのリファクタリングの妨げになる、さまざまな組織的または技術的な制約がある可能性があります。どのようにして対応すればよいでしょう。

レガシ コードのリファクタリングが難しいシナリオでは、このアプローチによって、少なくともビジネス ロジック用の単体テストを十分に作成して、システムの各モジュールの堅牢性を検証できる必要があります。TypeMock (learn.typemock.com、英語) などのモック フレームワークが登場してからしばらくたちます。モック フレームワークを使用すると、実際にコード ベースを変更しなくても、単体テストを作成できます。このアプローチは、特にサイズの大きいレガシ コードベースに対して非常に有効であることが認められています。

Pex には、これと同じ目的を達成できるモル (Mole) と呼ばれる機能が付属しています。モルを使用すると、実際にソース コードのリファクタリングを行わなくても、レガシ コード用の Pex 単体テストを生成できます。モルは、まさに、静的メソッドやシール クラスなど、モルなしではテストできないシステムの要素をテストするための機能です。

モルはスタブと同様に動作します。Pex によって、各メソッドのデリゲート プロパティを公開するモル型のコードが生成されます。デリゲートをアタッチし、次にモルをアタッチできます。その時点で、魔法でもかけられたかのように、すべてのカスタム デリゲートが Pex プロファイラーによって関連付けられます。

Pex は、.stubx ファイルの指定に従って、すべての静的インターフェイス、シール インターフェイス、パブリック インターフェイスに対してモルを自動的に作成します。Pex モルは、スタブ型と非常によく似ています (サンプルについては、コード ダウンロードを参照してください)。

モルを使用するのは、とても簡単です。モルのメソッドの実装を提供して、PUT でそれらを使用できます。モルによって実行時にスタブ実装が挿入されるため、モルを使用する場合は単体テストを生成するために、コードベースを変更する必要はまったくありません。

レガシの Fill メソッドにモルを使用してみましょう。

public class Order {

  public bool Fill(Product product, int quantity) {

    // Check if warehouse has any inventory

    Warehouse wareHouse = new Warehouse();

    if (wareHouse.HasInventory(product, quantity)) {

      // Subtract the quantity from the product 

      // in the warehouse

      wareHouse.Remove(product, quantity);

      return true;

    }



    return false;

  }

}

モル型を利用する Fill メソッド用の PUT を作成します (図 9 参照)。

図 9 Fill でのモル型の使用

[PexMethod]

public bool Fill([PexAssumeUnderTest]Order target,

  Product product, int quantity) {



  var products = new PexChosenIndexedValue<Product, int>(

    this, "products");

  // Attach a mole of WareHouse type

  var wareHouse = new MWarehouse {

    HasInventoryProductInt32 = (p, q) => {

      Assert.IsNotNull(p);

      return products.GetItem(p) >= q;

    }

  };



  // Run the fill method for the lifetime of the mole 

  // so it uses MWareHouse

  bool result = target.Fill(product, quantity);

  return result;

}

MWareHouse は、.stubx を利用してスタブとモルを生成したときに、Pex によって自動的に作成されたモル型です。MWareHouse 型の HasInventory デリゲートのカスタム実装を提供し、Fill メソッドを呼び出しています。Order 型のコンストラクターに Warehouse オブジェクトの実装を提供している部分がまったくないことに注意してください。Pex が、MWareHouse インスタンスを Order 型に実行時にアタッチします。PUT の有効期間中に、ブロック内に書き込まれたコードは、レガシ コードで Warehouse 実装が必要になるたびに、この MWareHouse 型の実装を利用します。

モルを使用する従来の単体テストを生成するときに、Pex はそれらの単体テストに [HostType(“Pex”)] 属性をアタッチして、Pex プロファイラーを使用してテストが実行されるようにします。これにより、モルを有効にすることができます。

まとめ

Pex のさまざまな機能と、それらの使用方法について説明しました。次は実際に PUT を実行して、結果を観察してみましょう。Order の Fill メソッドの探索を実行するには、PUT を右クリックして [Run Pex Exploration] (Pex 探索の結果) をクリックするだけです。任意のクラスやプロジェクト全体に探索を実行することもできます。

Pex 探索の実行中に、PUT クラス ファイルと併せて、部分クラスが作成されます。この部分クラスには、PUT に対して Pex が生成する標準の単体テストがすべて含まれます。Fill メソッドの場合は、さまざまなテスト入力を使用して標準の単体テストが Pex によって生成されます。図 10 に生成されたテストを示します。

図 10 Pex により生成されたテスト

[TestMethod]

[PexGeneratedBy(typeof(OrderTest))]

public void Fill15()

{

    Warehouse warehouse;

    Order order;

    Product product;

    bool b;

    warehouse = new Warehouse();

    order = OrderFactory.Create((IWareHouse)warehouse);

    product = new Product("Base ball", (string)null);

    b = this.Fill(order, product, 0);

    Assert.AreEqual<bool>(true, b);

}

[TestMethod]

[PexGeneratedBy(typeof(OrderTest))]

public void Fill16()

{

    Warehouse warehouse;

    Order order;

    Product product;

    bool b;

    warehouse = new Warehouse();

    order = OrderFactory.Create((IWareHouse)warehouse);

    product = new Product("Basket Ball", (string)null);

    b = this.Fill(order, product, 0);

    Assert.AreEqual<bool>(true, b);

}

[TestMethod]

[PexGeneratedBy(typeof(OrderTest))]

public void Fill17()

{

    Warehouse warehouse;

    Order order;

    Product product;

    bool b;

    warehouse = new Warehouse();

    order = OrderFactory.Create((IWareHouse)warehouse);

    product = new Product((string)null, (string)null);

    b = this.Fill(order, product, 1);

    Assert.AreEqual<bool>(false, b);

}

ここで確認すべき重要な点は、Product 型のバリエーションです。私は Product 型のファクトリを用意しませんでしたが、Pex によってこの型のバリエーションが作成できています。

また、生成されたテストにはアサーションが含まれていることに注意してください。PUT が値を返すと、Pex はテスト生成時に返された値を、生成されたテスト コードにアサーションとして埋め込みます。その結果、生成されたテストでは、多くの場合、プログラム コード内の他のアサーションに違反していなくても、将来問題を起こす変更を検出したり、実行エンジンのレベルで例外を生成できます。

Pex 探索の結果ウィンドウ (図 11 参照) に、Pex が生成した単体テストの詳細が表示されます。また、このウィンドウには、Pex が作成したファクトリと、探索中に発生したイベントについての情報も表示されます。図 11 では、2 つのテストで不合格になっています。どちらのテストに対しても、Pex は NullReferenceException を表示しています。これは、検証チェックがコード パスに実装していないために発生する一般的な問題の 1 つである可能性があります。検証が実装されていないと、いずれ運用中に例外が発生する可能性があります。

Pex Exploration Results

図 11 Pex 探索の結果

Pex はテストを生成するだけでなく、コードを改善できるように、コードの分析も行います。Pex は、コードの安定性を高めるための提案事項を提供します。これらの提案事項では、説明文だけでなく、問題の領域に対する実際のコードも示されます。ボタンをクリックすると、Pex はコードを実際のソース ファイルに挿入します。Pex 探索の結果ウィンドウで、不合格になったテストを選択します。右下隅に [Add Precondition] (前提条件の追加) というボタンが表示されます。このボタンをクリックすると、コードがソース ファイルに追加されます。

生成されたこれらのテストは、標準の MSTest 単体テストであり、Visual Studio のテスト エディターからも実行できます。エディターを開くと、Pex により生成されたすべてのテストが、標準の単体テストとして表示されます。Pex では、NUnit や xUnit など、その他の単体テスト フレーム用にも同様のテストを生成できます。

また、Pex には、カバレッジ レポートを生成するためのサポートが組み込まれています。カバレッジ レポートでは、テスト対象のコードの動的なカバレッジについての詳細情報が包括的に提供されます。レポートは、Visual Studio の [ツール] メニューの [Pex] (Pex) オプションから有効にします。また、レポートを開くには、探索の完了後に、[Pex] (Pex) メニューの [Views] (表示) をクリックし、[Report] (レポート) をクリックします。

テスト機能を使用できる状態にする

ここまで、ソース コードをわずかにリファクタリングするだけで、Pex がどのようにしてレガシ コードのコード カバレッジを生成できるかについて説明しました。Pex の便利な点は、開発者が単体テストを作成する必要がなく、単体テストが自動生成されるため、テスト作業全体の負荷が軽減されることです。

単体テストの実行中で最も困難な作業の 1 つは、テスト スイート自体のメンテナンスです。プロジェクトが進むに従い、通常は、既存のコードに多数の変更を実施します。単体テストはソース コードに依存しているため、コードが変更されると、対応する単体テストにも影響があります。コードの変更のために、コード カバレッジが無効または減退する可能性があります。時間が経過しても、テスト スイートの状態を有効に保つのは困難です。

このような状況で、Pex は便利です。Pex は探索を基盤としているため、コードベース内の新しい変更を検索して、検索された変更を基に新しいテスト ケースを作成できます。

回帰テストの最大の目的は、既存のコードに対する変更または追加によって、機能的なバグが生じたか新しいエラー状態が発生したために、コードベースに悪影響が出ていないかを検出することです。

Pex は回帰テスト スイートを自動生成できます。将来、この回帰テスト スイートを実行すると、プログラム コードのアサーションがエラーになる変更、実行エンジン レベルで例外 (NullReferenceException) を引き起こす変更、または生成されたテストに埋め込まれているアサーションがエラーになる変更を検出します。Pex 探索が新たに実行されるたびに、調査対象のコード用に単体テストが生成されます。動作の変更も Pex ではすべて検出され、この変更に合わせて、対応する単体テストが生成されます。

変更は避けられない

時間が経つにつれて、Fabrikam の開発者のチームは、ProductId 属性を Product クラスに追加して、会社が新製品をカタログに追加しても、一意に製品を識別できるようにすべきであることに気が付きました。

また、Order クラスは注文情報をデータ ストアに保存していなかったため、新しいプライベート メソッドの SaveOrders が Order クラスに追加されました。このメソッドは、製品の在庫がある場合、Fill メソッドによって呼び出されます。

変更された Fill メソッド クラスを次に示します。

public bool Fill(Product product, int quantity) {

  if (product == null) {

    throw new ArgumentException();

  }



  if (this.orderWareHouse.HasInventory(product, quantity)) {

    this.SaveOrder(product.ProductId, quantity);



    this.orderWareHouse.Remove(product, quantity);

  return true;

  }



  return false;

}

Fill メソッドのシグネチャは変わらないため、PUT を変更する必要はありません。Pex 探索を再び実行するだけです。Pex は探索を実行しますが、今回は新しい Product の定義を使用し、また ProductId も使用して入力を生成します。Pex は、Fill メソッドに対する変更を踏まえたうえで、新しいテスト スイートを生成します。コード カバレッジは 100% になりました。新しいコード パスと既存パスがすべて評価されています。

追加された ProductId フィールドのバリエーションと、Fill メソッドに対する変更をテストするための追加の単体テストも、Pex によって生成されます (図 12 参照)。ここで PexChooseStubBehavior が、スタブのフォールバック動作を設定しています。つまり、単に StubNotImplementedException をスローするのではなく、スタブ化されているメソッドは PexChoose を呼び出して、戻り値の候補を提供します。Visual Studio でテストを実行しても、コード カバレッジは 100% になります。

図 12 Pex により生成された追加の単体テスト

[TestMethod]

[PexGeneratedBy(typeof(OrderTest))]

public void Fill12()

{

    using (PexChooseStubBehavior.NewTest())

    {

      SIWareHouse sIWareHouse;

      Order order;

      Product product;

      bool b;

      sIWareHouse = new SIWareHouse();

      order = OrderFactory.Create((IWareHouse)sIWareHouse);

      product = new Product((string)null, (string)null);

      b = this.Fill(order, product, 0);

      Assert.AreEqual<bool>(false, b);

    }

}



[TestMethod]

[PexGeneratedBy(typeof(OrderTest))]

public void Fill13()

{

    using (PexChooseStubBehavior.NewTest())

    {

      SIWareHouse sIWareHouse;

      Order order;

      Product product;

      bool b;

      sIWareHouse = new SIWareHouse();

      order = OrderFactory.Create((IWareHouse)sIWareHouse);

      product = new Product((string)null, (string)null);

      IPexChoiceRecorder choices = PexChoose.NewTest();

      choices.NextSegment(3)

          .OnCall(0, 

      "SIWareHouse.global::FabrikamSports.IWareHouse.HasInventory(Product, Int32)")

          .Returns((object)true);

      b = this.Fill(order, product, 0);

      Assert.AreEqual<bool>(true, b);

    }

}

謝辞

この記事の執筆を勧めてくれた Peli de Halleux と Nikolai Tillman に感謝します。特に Peli de Halleux には、根気強いサポートと貴重な意見を頂いただけでなく、徹底的に校閲もしていただきました。

Nikhil Sachdeva は、マイクロソフトの OCTO-SE チームのソフトウェア開発エンジニアです。連絡先は blogs.msdn.com/erudition (英語) です。また、Pex についての質問は social.msdn.microsoft.com/Forums/ja/pex/threads (英語) に投稿できます。