テスト代替の連続性について検討する
テストをするために利用するコード「テスト代替(Test Doubles)」には様々な形態があり連続して利用されるという点を説明した良い記事です。Mocks Aren't Stubsと併せて読むのがお勧め。
ダミー
ダミーは必要なパラメータを埋めることだけを目的とする。実際に呼び出されないということから、コンパイラをとパスすために利用。作成が極めて容易だが有効性は低い。
internal class DummyShopDataAccess : IShopDataAccess { public decimal GetProductPrice(int productId) { throw new NotImplementedException(); } public void Save(int orderId, Order o) { throw new NotImplementedException(); } }
スタブ
クライアントが停止することなくメソッドを呼び出せるようにする。インターフェイスまたは基本クラスの最低限の実装。通常、void を返すメソッドは実装を一切含まないが、値を返すメソッドはハードコードされた値を返すことが一般的。作成が容易だが柔軟性に限界がある。メンバが正しく呼び出されたことを検証できない。
internal class StubShopDataAccess : IShopDataAccess { public decimal GetProductPrice(int productId) { return 25; } public void Save(int orderId, Order o) { } }
スパイ
メンバが正しく呼び出されたことを検証できる。 テスト スパイはスタブと似ているが、メンバを呼び出すインスタンスをクライアントに渡す点が異なる。柔軟性に限界がある。スパイを使った場合はスパイであることを意識した確認コードを記述する必要ある。
internal class SpyShopDataAccess : IShopDataAccess { private bool saveWasInvoked_; public decimal GetProductPrice(int productId) { switch (productId) { case 1234: return 25; case 2345: return 10; default: throw new ArgumentException("Unexpected productId"); } } public void Save(int orderId, Order o) { this.saveWasInvoked_ = true; } internal bool SaveWasInvoked { get { return this.saveWasInvoked_; } } }
フェイク
各種のシナリオに使用できるほぼ完全な実装を提供。 複雑になることからフェイク自体の単体テストを要する場合がある。 作成コストが高い。
internal class FakeShopDataAccess : IShopDataAccess { private ProductCollection products_; internal FakeShopDataAccess() { this.products_ = new ProductCollection(); } public decimal GetProductPrice(int productId) { if (this.products_.Contains(productId)) { return this.products_[productId].UnitPrice; } throw new ArgumentOutOfRangeException("productId"); } public void Save(int orderId, Order o) { } internal IListProducts { get { return this.products_; } } }
モック
モック ライブラリによって動的に作成され、構成に応じてダミー、スタブ、またはスパイのように動作する。 メンバが正しく呼び出されたことを検証できる。単体テストの観点からは透過的である。 課題は学習曲線の勾配が急で、トレーニングが必要である。
internal class MockShopDataAccess : IShopDataAccess { private ImplementationCallback implement_; internal MockShopDataAccess(ImplementationCallback callback) { this.implement_ = callback; } public decimal GetProductPrice(int productId) { MemberData member = new MemberData("GetProductPrice"); member.Parameters.Add(new ParameterData("productId", productId)); this.implement_(member); return (decimal)member.ReturnValue; } public void Save(int orderId, Order o) { MemberData member = new MemberData("Save"); member.Parameters.Add(new ParameterData("orderId", orderId)); member.Parameters.Add(new ParameterData("o", o)); this.implement_(member); } }