スタブを使用して単体テストでアプリケーションの各部分を相互に分離する

"スタブ型" は、Microsoft Fakes フレームワークによって提供される重要なテクノロジであり、これを使用すると、テスト対象のコンポーネントを、それが依存している他のコンポーネントから簡単に分離できます。 スタブは、テスト中に別のコンポーネントを置き換える短いコードとして機能します。 スタブを使用する主な利点は、一貫した結果を取得してテストを簡単に作成できるようになることです。 他のコンポーネントがまだ完全に機能していない場合であっても、スタブを使用するとテストを実行できます。

スタブを効果的に適用するには、アプリケーションの他の部分の具象クラスではなく、主にインターフェイスに依存するようにコンポーネントを設計することをお勧めします。 この設計アプローチによって分離が促進され、ある部分の変更によって別の部分の変更が必要になる可能性が低くなります。 テストに関しては、この設計パターンによって実際のコンポーネントの代わりにスタブ実装を使うことが可能になり、ターゲット コンポーネントの効果的な分離と正確なテストが促進されます。

例として、関連するコンポーネントを示す次の図を考えてみましょう。

Diagram of Real and Stub classes of StockAnalyzer.

この図で、テスト対象のコンポーネントは StockAnalyzer であり、これは通常 RealStockFeed という別のコンポーネントに依存しています。 しかし、RealStockFeed は、そのメソッドが呼び出されるたびに異なる結果を返すので、テストを難しくしています。 この変動性により、StockAnalyzer の一貫した信頼性の高いテストを保証することが困難になります。

テスト中にこの障害を克服するために、依存関係の挿入のプラクティスを採用できます。 この方法では、クラスをアプリケーションの別のコンポーネント内で明示的に言及しないようにコードを記述する必要があります。 代わりに、他のコンポーネントとスタブを使ってテスト目的で実装できるインターフェイスを定義します。

コードで依存関係の挿入を使う方法の例を次に示します。

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

スタブの制限事項

スタブについては、次の制限事項を確認してください。

スタブの作成: ステップ バイ ステップ ガイド

前の図に示した動機付けの例を使用して、この演習を始めましょう。

クラス ライブラリを作成する

クラス ライブラリを作成するには、次の手順に従います。

  1. Visual Studio を開き、クラス ライブラリ プロジェクトを作成します。

    Screenshot of Class Library project in Visual Studio.

  2. プロジェクト属性を構成します。

    • [プロジェクト名][StockAnalysis] に設定します。
    • [ソリューション名][StubsTutorial] に設定します。
    • プロジェクトの [ターゲット フレームワーク][.NET 8.0] に設定します。
  3. 既定のファイル Class1.cs を削除します。

  4. IStockFeed.cs」という名前の新しいファイルを追加して、次のインターフェイス定義にコピーします。

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. StockAnalyzer.cs」という名前の新しい別のファイルを追加して、次のクラス定義にコピーします。

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

テスト プロジェクトを作成する

演習用にテスト プロジェクトを作成します。

  1. ソリューションを右クリックして、「MSTest Test Project」という名前の新しいプロジェクトを追加します。

  2. プロジェクト名を [TestProject] に設定します。

  3. プロジェクトのターゲット フレームワークを [.NET 8.0] に設定します。

    Screenshot of Test project in Visual Studio.

Fakes アセンブリを追加する

プロジェクトの Fakes アセンブリを追加します。

  1. StockAnalyzer へのプロジェクト参照を追加します。

    Screenshot of the command Add Project Reference.

  2. Fakes アセンブリを追加します。

    1. ソリューション エクスプローラーで、アセンブリ参照を見つけます。

      • 古い .NET Framework プロジェクト (非 SDK スタイル) の場合は、単体テスト プロジェクトの [参照] ノードを展開します。

      • .NET Framework、.NET Core、または .NET 5.0 以降がターゲットである SDK スタイルのプロジェクトの場合は、 [依存関係] ノードを展開し、 [アセンブリ][プロジェクト] 、または [パッケージ] でフェイク化するアセンブリを見つけます。

      • Visual Basic で作業している場合、 [参照] ノードを表示するには、ソリューション エクスプローラー ツールバーの [すべてのファイルを表示] を選択します。

    2. スタブを作成する対象とするクラス定義が含まれているアセンブリを選択します。

    3. ショートカット メニューで、 [Fakes アセンブリの追加] を選択します。

      Screenshot of the command Add Fakes Assembly.

単体テストを作成する

次に、単体テストを作成します。

  1. 既定のファイル UnitTest1.cs を変更して、次の Test Method 定義を追加します。

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = componentUnderTest.GetContosoPrice();
    
            // Assert:
            // Verify the correct result in the usual way:
            Assert.AreEqual(priceToReturn, actualResult);
    
            // Verify that the component made the correct call:
            Assert.AreEqual("COOO", companyCodeUsed);
        }
    }
    

    ここで特別な魔法をかけます。それは StubIStockFeed クラスです。 参照アセンブリのそれぞれのインターフェイスに対して、Microsoft Fakes のメカニズムによってスタブ クラスが生成されます。 スタブ クラスの名前はインターフェイスの名前から派生します。プレフィックスとして "Fakes.Stub" が付き、パラメーターの型名が加わります。

    また、イベントおよびジェネリック メソッドについて、プロパティの getter および setter に対してもスタブが生成されます。 詳細については、「スタブを使用して単体テストでアプリケーションの各部分を相互に分離する」を参照してください。

    Screenshot of Solution Explorer showing all files.

  2. テスト エクスプローラーを開いてテストを実行します。

    Screenshot of Test Explorer.

さまざまな種類の型メンバーのスタブ

さまざまな種類の型のメンバーのスタブがあります。

メソッド

この例では、スタブ クラスのインスタンスにデリゲートをアタッチすることによって、メソッドをスタブすることができます。 スタブ型の名前は、メソッドとパラメーターの名前から派生されます。 たとえば、次の IStockFeed インターフェイスとそのメソッド GetSharePrice を考えてみます。

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

GetSharePriceString を使って GetSharePrice にスタブをアタッチします。

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

メソッドのスタブを指定しないと、戻り値の型の default value を返す関数が Fakes によって生成されます。 数値の場合、既定値は 0 です。 クラス型の場合、既定値は、C# では null、Visual Basic では Nothing です。

プロパティ

プロパティのゲッターとセッターは、個別のデリゲートとして公開され、個別にスタブできます。 例として、ValueIStockFeedWithProperty プロパティを考えます。

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

Value のゲッターとセッターをスタブして自動プロパティをシミュレートするには、次のコードを使用できます。

// unit test code
int i = 5;
var stub = new StubIStockFeedWithProperty();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

プロパティのセッターまたはゲッターにスタブ メソッドを指定しないと、値を格納するスタブが Fakes によって生成され、スタブ プロパティは単純な変数のように動作します。

イベント

イベントはデリゲート フィールドとして公開され、イベントのバッキング フィールドを呼び出すだけで任意のスタブされたイベントを発生させることができます。 次のようなインターフェイスをスタブするとします。

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Changed イベントを発生させるには、バッキング デリゲートを呼び出します。

// unit test code
var withEvents = new StubIStockFeedWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);

ジェネリック メソッド

ジェネリック メソッドをスタブするには、メソッドのインスタンス化が必要になるごとにデリゲートを指定します。 たとえば、ジェネリック メソッドを含む次のようなインターフェイスがあるとします。

interface IGenericMethod
{
    T GetValue<T>();
}

GetValue<int> のインスタンス化は次のようにスタブできます。

[TestMethod]
public void TestGetValue()
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

コードが他のインスタンス化で GetValue<T> を呼び出すと、スタブによってその動作が実行されます。

仮想クラスのスタブ

これまでの例では、スタブはインターフェイスから生成されていました。 しかし、仮想メンバーまたは抽象メンバーを持つクラスからスタブを生成することもできます。 たとえば次のような点です。

// Base class in application under test
public abstract class MyClass
{
    public abstract void DoAbstract(string x);
    public virtual int DoVirtual(int n)
    {
        return n + 42;
    }

    public int DoConcrete()
    {
        return 1;
    }
}

このクラスから生成されたスタブでは、DoConcrete()ではなく、DoAbstract()DoVirtual() のデリゲート メソッドを設定できます。

// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;

仮想メソッドのデリゲートを指定しない場合、Fakes では既定の動作を提供するか、基底クラスのメソッドを呼び出すことができます。 基本メソッドが呼び出されるようにするには、次のように CallBase プロパティを設定します。

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set - default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
// No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

スタブの既定の動作を変更する

生成された各スタブ型は、IStub.InstanceBehavior プロパティを通じて、IStubBehavior インターフェイスのインスタンスを保持します。 この動作は、カスタム デリゲートがアタッチされていないメンバーをクライアントが呼び出すたびに呼び出されます。 動作が設定されていない場合は、StubsBehaviors.Current プロパティによって返されたインスタンスが使用されます。 既定では、このプロパティは NotImplementedException 例外をスローする動作を返します。

動作は、任意のスタブ インスタンス上の InstanceBehavior プロパティを設定することによって、いつでも変更できます。 たとえば、次のスニペットでは、スタブが何も行わないか、戻り値の型の既定値 default(T) を返すように動作を変更します。

// unit test code
var stub = new StockAnalysis.Fakes.StubIStockFeed();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

また、動作が StubsBehaviors.Current プロパティで設定されていないすべてのスタブ オブジェクトでは、動作のグローバルな変更も可能です。

// Change default behavior for all stub instances where the behavior has not been set.
StubBehaviors.Current = BehavedBehaviors.DefaultValue;