Moqは、私の最新のプロジェクトに少し夢中になっています。最近バージョン4.0.10827にアップグレードしましたが、新しい動作のように見えることに気づきました。
基本的に、テストしているコードでモック関数(MakeCall
この例では)を呼び出すと、オブジェクト(TestClass
)を渡します。私がテストしているコードはTestClass
、の呼び出しの前後にオブジェクトに変更を加えますMakeCall
。コードが完成したら、MoqのVerify
関数を呼び出します。私の期待は、MoqがMakeCall
、おそらくディープクローニングのようなメカニズムを介して、私が渡した完全なオブジェクトを記録することです。MakeCall
このようにして、呼び出されることを期待している正確なオブジェクトで呼び出されたことを確認できます。残念ながら、これは私が見ているものではありません。
私はこれを以下のコードで説明しようとしています(うまくいけば、プロセスの中で少し明確にします)。
- まず、新しい
TestClass
オブジェクトを作成します。そのVar
プロパティはに設定され"one"
ます。 mockedObject
次に、テスト対象である モックオブジェクトを作成します。MakeCall
次に、のメソッドを呼び出しますmockedObject
(ちなみに、例で使用されているMachine.Specificationsフレームワークでは、When_Testing
クラス内のコードを上から下に読み取ることができます)。TestClass
次に、モックされたオブジェクトをテストして、Var
値が。で実際に呼び出されたことを確認します"one"
。予想通り、これは成功します。- 次に、プロパティをに
TestClass
再割り当てして、元のオブジェクトに変更を加えます。Var
"two"
- 次に、Moqが値が.のaで
MakeCall
呼び出されたとまだ考えているかどうかを確認します。私はそれが真実であると期待していますが、これは失敗します。TestClass
"one"
- 最後に、Moq
MakeCall
が実際TestClass
に値が。のオブジェクトによって呼び出されたと見なすかどうかをテストします"two"
。これは成功しますが、最初は失敗すると予想していました。
Moqが元のTestClass
オブジェクトへの参照のみを保持していることは明らかであり、その値を免責で変更することができ、テストの結果に悪影響を及ぼします。
テストコードに関するいくつかの注意事項。 IMyMockedInterface
私が嘲笑しているインターフェースです。 は私がメソッドTestClass
に渡しているクラスであるため、私が抱えている問題を示すために使用しています。MakeCall
最後にWhen_Testing
、テストコードを含む実際のテストクラスです。Machine.Specificationsフレームワークを使用しているため、奇妙な項目がいくつかあります('Because of'、'It should ...')。これらは、テストを実行するためにフレームワークによって呼び出される単なるデリゲートです。それらは簡単に削除でき、必要に応じて、含まれているコードを標準関数に配置する必要があります。すべてを許可するので、この形式のままにしましたValidate
完了するための呼び出し(「アレンジ、アサーションの実行」パラダイムと比較して)。明確にするために、以下のコードは私が問題を抱えている実際のコードではありません。私はこれと同じ振る舞いを複数の場所で見たので、単に問題を説明することを目的としています。
using Machine.Specifications;
// Moq has a conflict with MSpec as they both have an 'It' object.
using moq = Moq;
public interface IMyMockedInterface
{
int MakeCall(TestClass obj);
}
public class TestClass
{
public string Var { get; set; }
// Must override Equals so Moq treats two objects with the
// same value as equal (instead of comparing references).
public override bool Equals(object obj)
{
if ((obj != null) && (obj.GetType() != this.GetType()))
return false;
TestClass t = obj as TestClass;
if (t.Var != this.Var)
return false;
return true;
}
public override int GetHashCode()
{
int hash = 41;
int factor = 23;
hash = (hash ^ factor) * Var.GetHashCode();
return hash;
}
public override string ToString()
{
return MvcTemplateApp.Utilities.ClassEnhancementUtilities.ObjectToString(this);
}
}
[Subject(typeof(object))]
public class When_Testing
{
// TestClass is set up to contain a value of 'one'
protected static TestClass t = new TestClass() { Var = "one" };
protected static moq.Mock<IMyMockedInterface> mockedObject = new moq.Mock<IMyMockedInterface>();
Because of = () =>
{
mockedObject.Object.MakeCall(t);
};
// Test One
// Expected: Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
// Actual: Moq does verify that MakeCall was called with a TestClass with a value of 'one'.
// Result: This is correct.
It should_verify_that_make_call_was_called_with_a_value_of_one = () =>
mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());
// Update the original object to contain a new value.
It should_update_the_test_class_value_to_two = () =>
t.Var = "two";
// Test Two
// Expected: Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
// Actual: The Verify call fails, claiming that MakeCall was never called with a TestClass instance with a value of 'one'.
// Result: This is incorrect.
It should_verify_that_make_call_was_called_with_a_class_containing_a_value_of_one = () =>
mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());
// Test Three
// Expected: Moq should fail to verify that MakeCall was called with a TestClass with a value of 'two'.
// Actual: Moq actually does verify that MakeCall was called with a TestClass with a value of 'two'.
// Result: This is incorrect.
It should_fail_to_verify_that_make_call_was_called_with_a_class_containing_a_value_of_two = () =>
mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "two" }), moq.Times.Once());
}
これに関していくつか質問があります:
これは予想される動作ですか?
これは新しい動作ですか?
私が気付いていない回避策はありますか?
ベリファイを間違って使用していますか?
この状況を回避するためにMoqを使用するより良い方法はありますか?
何卒よろしくお願い申し上げます。
編集:
これは私がこの問題を経験した実際のテストとSUTコードの1つです。うまくいけば、それは説明として機能します。
// This is the MVC Controller Action that I am testing. Note that it
// makes changes to the 'searchProjects' object before and after
// calling 'repository.SearchProjects'.
[HttpGet]
public ActionResult List(int? page, [Bind(Include = "Page, SearchType, SearchText, BeginDate, EndDate")]
SearchProjects searchProjects)
{
int itemCount;
searchProjects.ItemsPerPage = profile.ItemsPerPage;
searchProjects.Projects = repository.SearchProjects(searchProjects,
profile.UserKey, out itemCount);
searchProjects.TotalItems = itemCount;
return View(searchProjects);
}
// This is my test class for the controller's List action. The controller
// is instantiated in an Establish delegate in the 'with_project_controller'
// class, along with the SearchProjectsRequest, SearchProjectsRepositoryGet,
// and SearchProjectsResultGet objects which are defined below.
[Subject(typeof(ProjectController))]
public class When_the_project_list_method_is_called_via_a_get_request
: with_project_controller
{
protected static int itemCount;
protected static ViewResult result;
Because of = () =>
result = controller.List(s.Page, s.SearchProjectsRequest) as ViewResult;
// This test fails, as it is expecting the 'SearchProjects' object
// to contain:
// Page, SearchType, SearchText, BeginDate, EndDate and ItemsPerPage
It should_call_the_search_projects_repository_method = () =>
s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsRepositoryGet,
s.UserKey, out itemCount), moq.Times.Once());
// This test succeeds, as it is expecting the 'SearchProjects' object
// to contain:
// Page, SearchType, SearchText, BeginDate, EndDate, ItemsPerPage,
// Projects and TotalItems
It should_call_the_search_projects_repository_method = () =>
s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsResultGet,
s.UserKey, out itemCount), moq.Times.Once());
It should_return_the_correct_view_name = () =>
result.ViewName.ShouldBeEmpty();
It should_return_the_correct_view_model = () =>
result.Model.ShouldEqual(s.SearchProjectsResultGet);
}
/////////////////////////////////////////////////////
// Here are the values of the three test objects
/////////////////////////////////////////////////////
// This is the object that is returned by the client.
SearchProjects SearchProjectsRequest = new SearchProjects()
{
SearchType = SearchTypes.ProjectName,
SearchText = GetProjectRequest().Name,
Page = Page
};
// This is the object I am expecting the repository method to be called with.
SearchProjects SearchProjectsRepositoryGet = new SearchProjects()
{
SearchType = SearchTypes.ProjectName,
SearchText = GetProjectRequest().Name,
Page = Page,
ItemsPerPage = ItemsPerPage
};
// This is the complete object I expect to be returned to the view.
SearchProjects SearchProjectsResultGet = new SearchProjects()
{
SearchType = SearchTypes.ProjectName,
SearchText = GetProjectRequest().Name,
Page = Page,
ItemsPerPage = ItemsPerPage,
Projects = new List<Project>() { GetProjectRequest() },
TotalItems = TotalItems
};