Language

shim を使用して単体テスト用にアプリを分離する

Shim 型は、Microsoft Fakes Framework で使用される 2 つの主要なテクノロジの 1 つであり、テスト中にアプリのコンポーネントを分離する際に利用されます。 特定のメソッドへの呼び出しをインターセプトして転送することで機能します。これにより、テスト内のカスタム コードに転送できます。 この機能を使用すると、これらのメソッドの結果を管理できるため、外部の条件に関係なく、各呼び出し中に結果の一貫性と予測性を確保できます。 このレベルの制御はテストプロセスを合理化し、より信頼でき、正確な結果を達成するのを助ける。

自分のコードと、ソリューションの一部ではないアセンブリとの間に境界を設ける必要がある場合は、シムを使用します。 ソリューションのコンポーネントを互いに分離することを目的とする場合は、 スタブ の使用をお勧めします。 スタブの詳細については、「 スタブを使用して、単体テストのためにアプリケーションの一部を互いに分離する」を参照してください。

この記事では、shim 型を使用して単体テスト コード内の特定のメソッドに呼び出しを転送するためのステップ バイ ステップ ガイドを提供します。

shim 使用時の型の制限を理解する

shim を使用する場合は、いくつかの制限があります。 .NET基底クラス内の特定のライブラリからのすべての型で使用することはできません。具体的には次のとおりです。

  • .NET Framework の mscorlib および System
  • .NET Core または .NET 5 以降における System.Runtime

テスト戦略を設計するときは、この制約を計画して、単体テストを成功させ、効果的なものにします。

シムを作成する

コンポーネントに System.IO.File.ReadAllLines メソッドの呼び出しが含まれているとします。

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

単体テスト用にコンポーネントを準備するには、次の手順の手順を実行します。

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

クラス ライブラリの新しいソリューションと初期プロジェクトを作成します。

  1. Visual Studio Start Window (File>Start Window) で、C# または Visual Basic のテンプレートを選択して、Class Library プロジェクトを作成します。

    Visual Studio の .NET Framework および C# 向けクラス ライブラリ プロジェクト テンプレートのスクリーンショット。

    Visual Studio 2022 の .NET Framework および C# 用クラス ライブラリ プロジェクト テンプレートのスクリーンショット。

  2. 新しいプロジェクトを構成します。

    • クラス ライブラリ Project name を HexFileReader に設定します。
    • ソリューション名をShimsTutorialに設定します。
    • Target framework を .NET Framework 4.8 に設定します。

    を選択してを作成します。

  3. プロジェクトが開いたら、Class1.cs で既定のファイル を見つけて、ファイルを削除します。

  4. HexFile.csという名前のファイルを追加し、次のクラス定義を入力します。

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

単体テスト プロジェクトを追加する

単体テスト用のソリューションに別のプロジェクトを追加します。

  1. ソリューション エクスプローラー で、ShimsTutorial ソリューションを右クリックし、Add>New Project を選択します。

  2. スタート ウィンドウで、テンプレートを選択して Unit Test Project project を作成します。

    Visual Studio の .NET Framework および C# 向け Microsoft 単体テスト プロジェクト テンプレートのスクリーンショット。

    Visual Studio 2022 の .NET Framework および C# 用 Microsoft 単体テスト プロジェクト テンプレートのスクリーンショット。

  3. 新しいプロジェクトを構成します。

    • 単体テスト Project name を TestProject に設定します。
    • Target framework を .NET Framework 4.8 に設定します。

    を選択してを作成します。

Fakes アセンブリを追加する

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

  1. ソリューション エクスプローラーで、TestProject ノードを展開し、References ノードを右クリックし、 参照を選択します。

    Visual Studio で、単体テスト プロジェクトからクラスへの参照を追加する方法を示すスクリーンショット。

    Visual Studio 2022 で、単体テスト プロジェクトからクラスへの参照を追加する方法を示したスクリーンショット。

    1. [参照マネージャー] ウィンドウの左側のウィンドウで、[プロジェクト] セクションを選択します。

    2. 中央のウィンドウで、 HexFileReader プロジェクトのチェック ボックスをオンにし、[ OK] を選択します。

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

    1. ソリューション エクスプローラー で、アセンブリの包含ノードを見つけます。

      • 以前の .NET Framework Project (SDK 以外のスタイル) の場合は、単体テスト project ノードを展開し、References ノードを展開します。

      • .NET Framework、.NET Core、または .NET 5 以降を対象とする SDK スタイルのプロジェクトの場合、 Dependencies ノードを展開して、アセンブリをAssemblies、Projects、または Packages の下に配置します。

      • Visual Basicの場合は、ソリューション エクスプローラー ツール バーの Show All Files を選択して、References ノードを表示します。

    2. Systemの定義を含むSystem.IO.File.ReadAllLines アセンブリを選択します。

    3. System ノードを右クリックし、[Fakes アセンブリの追加] を選択します。

      Visual Studio でプロジェクトに Fakes アセンブリを追加する方法を示すスクリーンショット。

      Visual Studio 2022 でプロジェクトに Fakes アセンブリを追加する方法を示したスクリーンショット。

    ビルド プロセスでは、シムでの使用がサポートされていない型に対して、警告とエラーが生成されます。

    ビルドが完了すると、ソリューション エクスプローラー が更新され、単体テスト プロジェクトの Fakes ノードが表示されます。

  3. Fakes\mscorlib.fakes ファイルを選択し、XML を置き換えて、サポートされていない型を除外します。

    <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
    <Assembly Name="mscorlib" Version="4.0.0.0"/>
    <StubGeneration>
         <Clear/>
    </StubGeneration>
    <ShimGeneration>
         <Clear/>
         <Add FullName="System.IO.File"/>
         <Remove FullName="System.IO.FileStreamAsyncResult"/>
         <Remove FullName="System.IO.FileSystemEnumerableFactory"/>
         <Remove FullName="System.IO.FileInfoResultHandler"/>
         <Remove FullName="System.IO.FileSystemInfoResultHandler"/>
         <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/>
         <Remove FullName="System.IO.FileSystemEnumerableIterator"/>
    </ShimGeneration>
    </Fakes>
    

単体テストを作成する

プロジェクトの単体テストを追加します。

  1. プロジェクト テンプレートによって提供される既定の TestProject\UnitTest1.cs ファイルを更新します。

    ファイル内のコードの次のセクションを見つけて、指定されたスニペットに置き換えます。

    [TestMethod]
    public void TestMethod1()
    {
    }
    

    置換用スニペット:

    [TestMethod]
    public void TestFileReadAllLine()
    {
       using (ShimsContext.Create())
       {
          // Arrange
          System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" };
    
          // Act
          var target = new HexFile("this_file_doesnt_exist.txt");
    
          Assert.AreEqual(3, target.Records.Length);
       }
    }
    
  2. 単体テストのすべての Fakes アセンブリを表示するには、ソリューション エクスプローラー メニュー バーで Show All Files を選択します。

    Fakes アセンブリを含むすべてのファイルが表示された、Visual Studio 2022 のソリューション エクスプローラーのスクリーンショット。

  3. テスト エクスプローラーを開き、テストを実行します。

各 shim コンテキストを適切に破棄することが重要です。 経験則として、ShimsContext.Create ステートメント内で using メソッドを呼び出して、登録された shim を適切にクリアします。 たとえば、 DateTime.Now メソッドを常に 2000 年 1 月 1 日を返すデリゲートに置き換えるテスト メソッドの shim を登録できます。 テスト メソッドで登録済みの shim をクリアし忘れた場合、テスト実行の残りの部分では常に 2000 年 1 月 1 日が DateTime.Now 値として返されます。 この結果は驚くべきものであり、混乱を招く可能性があります。

shim クラスの名前付け規則を確認する

shim クラス名では、 Fakes.Shim プレフィックスの後に元の型名が続きます。 パラメーター名がメソッド名に追加されます。 (アセンブリ参照を System.Fakesに追加する必要はありません)。

System.IO.File.ReadAllLines(path);

System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

シムの仕組みを理解する

Shim は、テスト対象のアプリケーションのコードベースに 迂回 を導入することによって動作します。 元のメソッドが呼び出されるたびに、Fakes システムが介入してその呼び出しをリダイレクトし、元のメソッドの代わりにカスタムの shim コードが実行されます。

これらの回り道は実行時に動的に作成および削除されることに注意してください。 回り道は、常に ShimsContextの有効期間内に作成する必要があります。 ShimsContext が破棄されると、その中で作成されたアクティブな shim も削除されます。 この設計を効率的に管理するには、 using ステートメント内に迂回の作成をカプセル化することをお勧めします。

さまざまな種類のメソッド向けのシムを確認する

Shims は、さまざまな種類のメソッドをサポートします。

静的メソッド

静的メソッドにシムを適用する場合、シムを保持するプロパティはシム型内に配置されます。 これらのプロパティは、ターゲット メソッドにデリゲートをアタッチするために使用されるセッターのみを所有します。

たとえば、MyClass という静的メソッドを持つ MyMethod というクラスの場合:

//Code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

常に 5 を返すような shim を MyMethod に取り付けることができます。

// Unit test code
ShimMyClass.MyMethod = () => 5;

インスタンスメソッド(すべてのインスタンス用)

静的メソッドと同様に、インスタンス メソッドもすべてのインスタンスに対してシミングできます。 これらの shim を保持するプロパティは、混乱を防ぐために、 AllInstances という名前の入れ子になった型に配置されます。

インスタンス メソッド MyClass を持つクラス MyMethod の場合:

// Code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

shim を MyMethod にアタッチして、インスタンスに関係なく一貫して 5 を返すことができます。

// Unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

次のスニペットは、 ShimMyClassの生成された型構造を示しています。

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public static class AllInstances {
      public static Func<MyClass, int>MyMethod {
         set {
            ...
         }
      }
   }
}

このシナリオでは、Fakes はランタイム インスタンスをデリゲートの最初の引数として渡します。

インスタンス メソッド (単一ランタイム インスタンス)

インスタンス メソッドは、呼び出しの受信側に応じて、異なるデリゲートを使用してシミングすることもできます。 この方法では、同じインスタンス メソッドが型のインスタンスごとに異なる動作を示すことができます。 これらの shim を保持するプロパティは、shim 型自体のインスタンス メソッドです。 インスタンス化されたシム型はそれぞれ、シム化された型の生のインスタンスに関連付けられます。

たとえば、インスタンス メソッド MyClass を持つクラス MyMethod があるとします。

// Code under test
public class MyClass {
   public int MyMethod() {
      ...
   }
}

1 つ目が一貫して 5 を返し、2 つ目が一貫して 10 を返すように、 MyMethod に対して 2 つの shim 型を作成できます。

// Unit test code
var myClass1 = new ShimMyClass()
{
   MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

次のスニペットは、 ShimMyClassの生成された型構造を示しています。

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public Func<int> MyMethod {
      set {
         ...
      }
   }
   public MyClass Instance {
      get {
         ...
      }
   }
}

実際の shimmed 型インスタンスには、 Instance プロパティを使用してアクセスできます。

// Unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

shim 型には、shim 化された型への暗黙的な変換も含まれているため、shim 型を直接使用できます。

// Unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // Implicit cast retrieves the runtime instance

shim コンストラクターを探索する

コンストラクターもシミングの対象です。 今後作成されるオブジェクトに shim 型をアタッチするために、シミングすることができます。 たとえば、すべてのコンストラクターは、shim 型内で Constructor という名前の静的メソッドとして表されます。

整数を受け取るコンストラクターを持つクラス MyClass について考えてみましょう。

public class MyClass {
   public MyClass(int value) {
      this.Value = value;
   }
   ...
}

このコンストラクターの shim 型は、コンストラクターに渡される値に関係なく、 Value getter が呼び出されたときに将来のすべてのインスタンスが -5 を返すように設定できます。

// Unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
   var shim = new ShimMyClass(@this) {
      ValueGet = () => -5
   };
};

各シム型は、2 種類のコンストラクターを公開します:

  • 新しいインスタンスが必要な場合は、既定のコンストラクターを使用します。
  • コンストラクター shim がある場合は、shimmed インスタンスを引数として受け取るコンストラクターを使用します。
// Unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

次のコードは、 ShimMyClassに対して生成された型を示しています。

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
   public static Action<MyClass, int> ConstructorInt32 {
      set {
         ...
      }
   }

   public ShimMyClass() { }
   public ShimMyClass(MyClass instance) : base(instance) { }
   ...
}

ベース メンバーにアクセスする

基底型の shim を作成することで、基底メンバーの shim プロパティにアクセスできます。 次に、子インスタンスを基本 shim クラスのコンストラクターに入力します。

たとえば、インスタンス メソッド MyBaseとサブタイプ MyMethodを持つクラス MyChildについて考えてみます。

public abstract class MyBase {
   public int MyMethod() {
       ...
   }
}

public class MyChild : MyBase {
}

新しいMyBase shim を開始することで、ShimMyBaseの shim を設定できます。

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

基本 shim コンストラクターにパラメーターとして子 shim を渡すと、子 shim 型が子インスタンスに暗黙的に変換されることに注意してください。

ShimMyChildとShimMyBaseに対して生成される型の構造は、次のコードのようになります。

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
   public ShimMyChild() { }
   public ShimMyChild(Child child)
       : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
   public ShimMyBase(Base target) { }
   public Func<int> MyMethod
   { set { ... } }
}

静的コンストラクター

Shim 型は、型の静的コンストラクターを shim するための静的メソッド StaticConstructor を提供します。 静的コンストラクターは 1 回だけ実行されるため、型のメンバーにアクセスする前に shim が構成されていることを確認する必要があります。

ファイナライザー

ファイナライザーは Fakes ではサポートされていません。

プライベート メソッド

Fakes コード ジェネレーターは、シグネチャに表示可能な型 (つまり、パラメーター型と戻り値の型が表示される) のみを持つプライベート メソッドの shim プロパティを作成します。

インターフェイスの結合

shimmed 型がインターフェイスを実装すると、コード ジェネレーターは、そのインターフェイスのすべてのメンバーを一度にバインドできるメソッドを出力します。

たとえば、MyClassを実装するクラス IEnumerable<int>があるとします。

public class MyClass : IEnumerable<int> {
   public IEnumerator<int> GetEnumerator() {
       ...
   }
   ...
}

IEnumerable<int> メソッドを呼び出すことで、MyClass 内の Bind の実装をシムできます。

// Unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

ShimMyClassの生成された型構造は、次のコードのようになります。

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public ShimMyClass Bind(IEnumerable<int> target) {
       ...
   }
}

既定の動作を変更する

生成された各 shim 型には、 IShimBehavior インターフェイスのインスタンスが含まれています。 ShimBase<T>.InstanceBehavior プロパティを使用してアクセスできます。 この動作は、クライアントが明示的にシミングされていないインスタンス メンバーを呼び出すたびに呼び出されます。

既定では、特定の動作が設定されていない場合、デザインは静的な ShimBehaviors.Current プロパティによって返されるインスタンスを使用します。これは通常、 NotImplementedException 例外をスローします。

shim インスタンスの InstanceBehavior プロパティを調整することで、この動作はいつでも変更できます。

たとえば、何も行わないか、戻り値の型の既定値を返すように動作を変更 default(T)。

// Unit test code
var shim = new ShimMyClass();

//Return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

静的InstanceBehavior プロパティを設定することで、ShimBehaviors.Current プロパティが明示的に定義されていないすべてのシミングされたインスタンスの動作をグローバルに変更することもできます。

// Unit test code
// Change default shim for all shim instances where the behavior isn't set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

外部依存関係との対話を識別する

コードが外部システムまたは依存関係 ( environment と呼ばれます) と対話するタイミングを特定するために、shim を使用して、型のすべてのメンバー (静的メソッドを含む) に特定の動作を割り当てることができます。 shim 型の静的ShimBehaviors.NotImplemented プロパティに対してBehavior動作を設定すると、明示的に shimmed ではないその型のメンバーにアクセスすると、NotImplementedExceptionがスローされます。 この結果は、テスト中に役立つシグナルとして機能し、コードが外部システムまたは依存関係にアクセスしようとしていることを示します。

単体テスト コードでこの形式の shim を設定する方法の例を次に示します。

// Unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

便宜上、短縮形メソッドも提供され、同じ効果が得られます。

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

shim メソッド内から元のメソッドを呼び出す

シナリオによっては、shim メソッドの実行中に元のメソッドの実行が必要になる場合があります。 たとえば、メソッドに渡されたファイル名を検証した後、ファイル システムにテキストを書き込む場合があります。

この状況を処理する方法の 1 つは、デリゲートと ShimsContext.ExecuteWithoutShims() メソッドを使用して元のメソッドの呼び出しをカプセル化することです。

// Unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

     Console.WriteLine("enter");
     File.WriteAllText(fileName, content);
     Console.WriteLine("leave");
  });
};

または、shim を null 化し、元のメソッドを呼び出して、shim を復元することもできます。

// Unit test code
ShimsDelegates.Action<string, string> shim = null;

shim = (fileName, content) => {
   try {
      Console.WriteLine("enter");
      // Remove shim in order to call original method
      ShimFile.WriteAllTextStringString = null;
      File.WriteAllText(fileName, content);
   }
   finally
   {
      // Restore shim
      ShimFile.WriteAllTextStringString = shim;
      Console.WriteLine("leave");
   }
};

// Initialize the shim
ShimFile.WriteAllTextStringString = shim;

shim 型を使用して並行処理に対処する

Shim 型は AppDomain 内のすべてのスレッドで動作し、スレッド アフィニティを持っていません。 コンカレンシーをサポートするテスト ランナーを使用する予定の場合は、この特性に留意することが重要です。 Fakes ランタイムではこの制限は適用されませんが、shim 型を含むテストは同時に実行できないことに注目してください。

System.Environment でシムを使用する

System.Environment クラスを shim するには、mscorlib.fakes ファイルを変更する必要があります。

\<Assembly>要素を見つけ、要素定義の後に次の内容を追加します。

<ShimGeneration>
   <Add FullName="System.Environment"/>
</ShimGeneration>

変更を加えてソリューションをリビルドした後、 System.Environment クラスのメソッドとプロパティを shimming で使用できるようになりました。

GetCommandLineArgsGet メソッドに動作を割り当てる方法の例を次に示します。

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

これらの変更を行うと、コードがシステム環境変数とどのように対話するかを制御およびテストする機能が得られます。これは、包括的な単体テストに不可欠なツールです。