基本上,当我在我正在测试的代码中调用我的模拟函数(在本例中为MakeCall)时,我传入了一个对象(TestClass).我正在测试的代码在调用MakeCall之前和之后对TestClass对象进行了更改.代码完成后,我会调用Moq的Verify函数.我的期望是,Moq将记录我传入MakeCall的完整对象,可能是通过深度克隆等机制.通过这种方式,我将能够验证MakeCall是否被我希望调用的确切对象调用.不幸的是,这不是我所看到的.
我试图在下面的代码中说明这一点(希望在此过程中澄清一点).
>我首先创建一个新的TestClass对象.它的Var属性设置为“1”.
>然后我创建了模拟对象mockedObject,这是我的测试主题.
>然后我调用mockedObject的MakeCall方法(顺便说一下,示例中使用的Machine.Specifications框架允许从上到下读取When_Testing类中的代码).
>然后我测试模拟对象以确保它确实使用VarC值为“1”的TestClass调用.正如我所预料的那样,这成功了.
>然后我通过将Var属性重新分配给“two”来更改原始TestClass对象.
>然后我继续尝试验证Moq是否仍然认为使用值为“1”的TestClass调用了MakeCall.这失败了,虽然我期待它是真的.
>最后,我测试看看Moq是否认为MakeCall实际上是由一个值为“2”的TestClass对象调用的.这成功了,虽然我最初预计它会失败.
对我来说,似乎很清楚Moq只保留对原始TestClass对象的引用,允许我改变其值而不受惩罚,对我的测试结果产生负面影响.
关于测试代码的一些注意事项. IMyMockedInterface是我嘲笑的界面. TestClass是我传递给MakeCall方法的类,因此用于演示我遇到的问题.最后,When_Testing是包含测试代码的实际测试类.它使用Machine.Specifications框架,这就是为什么有一些奇怪的项目(‘因为’,’它应该…’).这些只是框架调用以执行测试的委托.如果需要,应该很容易删除它们并将包含的代码放入标准函数中.我把它保留为这种格式,因为它允许所有Validate调用完成(与’Arrange,Act Assert’范例相比).只是为了澄清,下面的代码不是我遇到问题的实际代码.它只是为了说明问题,因为我在多个地方看到了同样的行为.
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代码之一.希望它可以作为澄清.
// 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,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,EndDate,ItemsPerPage,// Projects and TotalItems It should_call_the_search_projects_repository_method = () => s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsResultGet,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,Page = Page,ItemsPerPage = ItemsPerPage }; // This is the complete object I expect to be returned to the view. SearchProjects SearchProjectsResultGet = new SearchProjects() { SearchType = SearchTypes.ProjectName,ItemsPerPage = ItemsPerPage,Projects = new List<Project>() { GetProjectRequest() },TotalItems = TotalItems };
解决方法
从逻辑的角度来看,我认为这是一个合理的期望.您正在执行值为Y的操作X.如果您询问模拟“我是否执行了值为Y的操作X”,则无论系统的当前状态如何,您都希望它为“是”.
总结您遇到的问题:
>首先使用引用类型参数在模拟对象上调用方法.
> Moq保存有关调用的信息以及传入的引用类型参数.
>然后,您询问Moq是否使用等于您传入的引用的对象调用该方法一次.
> Moq使用与提供的参数匹配的参数检查其历史记录以获取对该方法的调用,并回答是.
>然后,您将作为参数传递的对象修改为模拟上的方法调用.
>参考Moq的内存空间在其历史记录中保持更改为新值.
>然后,您询问Moq是否一次调用该方法的对象不等于其所持有的引用.
> Mock使用与提供的参数匹配的参数检查其历史记录以获取对该方法的调用,并报告否.
试图回答您的具体问题:
>这是预期的行为吗?
我会说不.
>这是新的行为吗?
我不知道,但令人怀疑的是,该项目一次会有行为促进这一点,后来被修改为仅允许仅模拟每个模拟一次使用的简单场景.
>有没有我不知道的解决方法?
我会回答这两种方式.
从技术角度来看,解决方法是使用Test Spy而不是Mock.通过使用Test Spy,您可以记录传递的值并使用您自己的策略来记住状态,例如进行深度克隆,序列化对象,或者只存储您关心的特定值以便稍后进行比较.
从测试的角度来看,我建议您遵循“Use The Front Door First”原则.我认为有一段时间进行基于状态的测试以及基于交互的测试,但您应该尽量避免将自己与实现细节结合起来,除非交互是场景的一个重要部分.在某些情况下,您感兴趣的场景主要是关于互动(“在账户之间转移资金”),但在其他情况下,您真正关心的是获得正确的结果(“提取10美元”).对于控制器的规范,这似乎属于查询类别,而不是命令类别.只要它们是正确的,你并不真正关心它如何得到你想要的结果.因此,我建议在这种情况下使用基于状态的测试.如果另一个规范涉及对系统发出命令,那么最终可能仍然应该考虑使用前门解决方案,但是进行基于交互的测试可能是必要或重要的.只是我的想法.
>我使用验证错误吗?
您正在使用Verify()方法,它只是不支持您使用它的方案.
>有没有更好的方法使用Moq来避免这种情况?
我不认为Moq目前正在实施以处理这种情况.
希望这可以帮助,
德里克格里尔
http://derekgreer.lostechies.com
http://aspiringcraftsman.com
@derekgreer