意外的最小起订量验证行为
起订量让我对我的最新项目有点疯狂。我最近升级到版本 4.0.10827,我注意到在我看来这是一个新行为。
基本上,当我在正在测试的代码中调用模拟函数(在本例中为 MakeCall
)时,我会传入一个对象 (TestClass
)。我正在测试的代码在调用 MakeCall
之前和之后对 TestClass
对象进行了更改。代码完成后,我将调用 Moq 的 Verify
函数。我的期望是 Moq 将记录我传递到 MakeCall 的完整对象,也许是通过深度克隆等机制。这样,我将能够验证 MakeCall
是否使用我期望调用的确切对象进行调用。不幸的是,这不是我所看到的。
我尝试在下面的代码中说明这一点(希望在此过程中对其进行澄清)。
- 我首先创建一个新的
TestClass
对象。其Var
属性设置为"one"
。 - 然后,我创建模拟对象
mockedObject
,这是我的测试主题。 - 然后,我调用
mockedObject
的MakeCall
方法(顺便说一下,示例中使用的 Machine.Specifications 框架允许使用When_Testing
类中的代码从上到下阅读)。 - 然后,我测试模拟对象,以确保它确实是通过
TestClass
调用的,且Var
值为“one”
。正如我所期望的那样,这成功了。 - 然后,我通过将
Var
属性重新分配给"two"
来对原始TestClass
对象进行更改。 - 然后,我继续尝试验证 Moq 是否仍然认为
MakeCall
是使用值为“one”
的TestClass
调用的。尽管我希望它是真的,但还是失败了。 - 最后,我测试看看 Moq 是否认为
MakeCall
实际上是由值为“two”
的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, 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
};
Moq has been driving me a bit crazy on my latest project. I recently upgraded to version 4.0.10827, and I'm noticing what seems to me to be a new behavior.
Basically, when I call my mocked function (MakeCall
, in this example) in the code I am testing, I am passing in an object (TestClass
). The code I am testing makes changes to the TestClass
object before and after the call to MakeCall
. Once the code has completed, I then call Moq's Verify
function. My expectation is that Moq will have recorded the complete object that I passed into MakeCall
, perhaps via a mechanism like deep cloning. This way, I will be able to verify that MakeCall
was called with the exact object I am expecting it to be called with. Unfortunately, this is not what I'm seeing.
I attempt to illustrate this in the code below (hopefully, clarifying it a bit in the process).
- I first create a new
TestClass
object. ItsVar
property is set to"one"
. - I then create the mocked object,
mockedObject
, which is my test subject. - I then call the
MakeCall
method ofmockedObject
(by the way, the Machine.Specifications framework used in the example allows the code in theWhen_Testing
class to be read from top to bottom). - I then test the mocked object to ensure that it was indeed called with a
TestClass
with aVar
value of"one"
. This succeeds, as I expected it to. - I then make a change to the original
TestClass
object by re-assigning theVar
property to"two"
. - I then proceed to attempt to verify if Moq still thinks that
MakeCall
was called with aTestClass
with a value of"one"
. This fails, although I am expecting it to be true. - Finally, I test to see if Moq thinks
MakeCall
was in fact called by aTestClass
object with a value of"two"
. This succeeds, although I would initially have expected it to fail.
It seems pretty clear to me that Moq is only holding onto a reference to the original TestClass
object, allowing me to change its value with impunity, adversely affecting the results of my testing.
A few notes on the test code. IMyMockedInterface
is the interface I am mocking. TestClass
is the class I am passing into the MakeCall
method and therefore using to demonstrate the issue I am having. Finally, When_Testing
is the actual test class that contains the test code. It is using the Machine.Specifications framework, which is why there are a few odd items ('Because of', 'It should...'). These are simply delegates that are called by the framework to execute the tests. They should be easily removed and the contained code placed into a standard function if that is desired. I left it in this format because it allows all Validate
calls to complete (as compared to the 'Arrange, Act Assert' paradigm). Just to clarify, the below code is not the actual code I am having problems with. It is simply intended to illustrate the problem, as I have seen this same behavior in multiple places.
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());
}
I have a few questions regarding this:
Is this expected behavior?
Is this new behavior?
Is there a workaround that I am unaware of?
Am I using Verify incorrectly?
Is there a better way of using Moq to avoid this situation?
I thank you humbly for any assistance you can provide.
Edit:
Here is one of the actual tests and SUT code that I experienced this problem with. Hopefully it will act as clarification.
// 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
};
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
最终,您的问题是模拟框架是否应该拍摄您在与模拟交互时使用的参数的快照,以便它可以准确地记录系统在交互时所处的状态,而不是参数在交互时可能所处的状态。验证点。
我想说,从逻辑的角度来看,这是一个合理的期望。您正在执行具有值 Y 的操作 X。如果您询问模拟“我是否执行了具有值 Y 的操作 X”,您希望它说“是”,无论系统的当前状态如何。
总结一下您遇到的问题:
您首先使用引用类型参数调用模拟对象上的方法。
Moq 保存有关调用的信息以及传入的引用类型参数。
然后,您询问 Moq 是否使用等于您传入的引用的对象调用该方法一次。
然后,您可以修改作为参数传递给模拟上的方法调用的对象。
参考 Moq 的内存空间在其历史记录中保存对新值的更改。
然后,您询问 Moq 是否使用不等于其持有的引用的对象调用该方法一次。
Mock 使用与所提供的参数匹配的参数检查其对该方法的调用历史记录,并报告否。
要尝试回答您的具体问题:
这是预期的行为吗?
我会说不。
这是新行为吗?
我不知道,但值得怀疑的是,该项目曾经有过促进这一点的行为,后来被修改为只允许每个模拟仅验证一次使用的简单场景。
有我不知道的解决方法吗?
我会用两种方式回答这个问题。
从技术角度来看,解决方法是使用测试间谍而不是模拟。通过使用测试间谍,您可以记录传递的值并使用您自己的策略来记住状态,例如进行深度克隆、序列化对象或仅存储您关心的特定值以便稍后进行比较。嗯>
从测试的角度来看,我建议您遵循以下原则 " 使用首先是前门”。我相信有时会进行基于状态的测试和基于交互的测试,但您应该尽量避免将自己与实现细节耦合,除非交互是场景的重要组成部分。在某些情况下,您感兴趣的场景主要是关于交互(“在帐户之间转移资金”),但在其他情况下,您真正关心的是获得正确的结果(“提取 10 美元”)。就控制器的规范而言,这似乎属于查询类别,而不是命令类别。你并不关心它如何得到你想要的结果,只要它们是正确的。因此,我建议在这种情况下使用基于状态的测试。如果另一个规范涉及针对系统发出命令,则最终可能仍然存在您应该首先考虑使用的前门解决方案,但进行基于交互的测试可能是必要或重要的。只是我的想法。
我是否错误地使用了验证?
您正确使用了Verify()方法,它只是不支持您使用它的场景。
是否有更好的方法使用Moq来避免这种情况?
我认为当前没有实现 Moq 来处理这种情况。
希望这会有所帮助,
Derek Greer
http://derekgreer.lostechies.com
http://aspiringcraftsman.com
@derekgreer
Ultimately, your question is whether a mocking framework should take snapshots of the parameters you use when interacting with the mocks so that it can accurately record the state the system was in at the point of interaction rather than the state the parameters might be in at the point of verification.
I would say this is a reasonable expectation from a logical point of view. You are performing action X with value Y. If you ask the mock "Did I perform action X with value Y", you expect it to say "Yes" regardless of the current state of the system.
To summarize the problem you are running into:
You first invoke a method on a mock object with a reference type parameter.
Moq saves information about the invocation along with the reference type parameter passed in.
You then ask Moq if the method was called one time with an object equal to the reference you passed in.
Moq checks its history for a call to that method with a parameter that matches the supplied parameter and answers yes.
You then modify the object that you passed as the parameter to the method call on the mock.
The memory space of the reference Moq is holding in its history changes to the new value.
You then ask Moq if the method was called one time with an object that isn't equal to the reference its holding.
Mock checks its history for a call to that method with a parameter that matches the supplied parameter and reports no.
To attempt to answer your specific questions:
Is this expected behavior?
I would say no.
Is this new behavior?
I don't know, but it's doubtful the project would have at one time had behavior that facilitated this and was later modified to only allow the simple scenario of only verifying a single usage per mock.
Is there a workaround that I am unaware of?
I'll answer this two ways.
From a technical standpoint, a workaround would be to use a Test Spy rather than a Mock. By using a Test Spy, you can record the values passed and use your own strategy for remembering the state, such as doing a deep clone, serializing the object, or just storing the specific values you care about to be compared against later.
From a testing standpoint, I would recommend that you follow the principle "Use The Front Door First". I believe there is a time for state-based testing as well as interaction-based testing, but you should try to avoid coupling yourself to the implementation details unless the interaction is an important part of the scenario. In some cases, the scenario you are interested in will be primarily about interaction ("Transfer funds between accounts"), but in other cases all you really care about is getting the correct result ("Withdraw $10"). In the case of the specification for your controller, this seems to fall into the query category, not the command category. You don't really care how it gets the results you want as long as they are correct. Therefore, I would recommend using state-based testing in this case. If another specification concerns issuing a command against the system, there may still end up being a front door solution which you should consider using first, but it may be necessary or important to do interaction based testing. Just my thoughts though.
Am I using Verify incorrectly?
You are using the Verify() method correctly, it just doesn't support the scenario you are using it for.
Is there a better way of using Moq to avoid this situation?
I don't think Moq is currently implemented to handle this scenario.
Hope this helps,
Derek Greer
http://derekgreer.lostechies.com
http://aspiringcraftsman.com
@derekgreer
首先,您可以通过声明来避免
Moq
和MSpec
之间的冲突,然后当您想使用时,只需在前面加上
Moq.
前缀即可Moq 的It
,例如Moq.It.IsAny<>()
。关于你的问题。
注意:这不是原始答案,而是 OP 在问题中添加一些真实示例代码后编辑的答案
我一直在尝试你的示例代码我认为这与 MSpec 的关系比 Moq 的关系更大。显然(我也不知道这一点),当您在
It
委托中修改 SUT(被测系统)的状态时,更改会被记住。现在发生的事情是:因为
委托正在运行它
委托一个接一个地运行。如果改变状态,下面的It
将永远不会看到中的设置因为
。因此你的测试失败了。我尝试使用
SetupForEachSpecificationAttribute
标记您的规范:该属性正如其名称所示:它将运行您的
Establish
和Because
在每个它
之前。添加该属性使规范的行为符合预期:3 次成功,1 次失败(使用 Var =“two”进行验证)。SetupForEachSpecificationAttribute
能否解决您的问题,还是在每次It
不适合您的测试后重置?仅供参考:我正在使用
Moq v4.0.10827.0
和MSpec v0.4.9.0
免费提示 #2:如果您正在使用 Mspec 测试 ASP.NET MVC 应用程序你可能想看看 James Broome 的 MVC MSpec 扩展
First, you can avoid the conflict between
Moq
andMSpec
by declaringThen you'll only need to prefix with
Moq.
when you want to use Moq'sIt
, for exampleMoq.It.IsAny<>()
.Onto your question.
Note: This is not the original answer but an edited one after the OP added some real example code to the question
I've been trying out your sample code and I think it's got more to do with MSpec than Moq. Apparently (and I didn't know this either), when you modify the state of your SUT (System Under Test) inside an
It
delegate the changes gets remembered. What is happening now is:Because
delegate is runIt
delegates are run, one after the other. If one changes the state, the followingIt
will never see the set up in theBecause
. Hence your failed test.I've tried marking your spec with the
SetupForEachSpecificationAttribute
:The attribute does as its name says: It will run your
Establish
andBecause
before everyIt
. Adding the attribute made the spec behave as expected: 3 Successes, one fail (the verification that with Var = "two").Would the
SetupForEachSpecificationAttribute
solve your problem or is resetting after everyIt
not acceptable for your tests?FYI: I'm using
Moq v4.0.10827.0
andMSpec v0.4.9.0
Free tip #2: If you're testing ASP.NET MVC apps with Mspec you might want to take a look at James Broome's MSpec extensions for MVC