起订量让我对我的最新项目有点抓狂。我最近升级到版本 4.0.10827,我注意到在我看来这是一种新行为。
基本上,当我在我正在测试的代码中调用我的模拟函数(MakeCall
在这个例子中)时,我传入了一个对象(TestClass
)。我正在测试的代码TestClass
在调用MakeCall
. 代码完成后,我将调用 Moq 的Verify
函数。我的期望是 Moq 将记录我传入的完整对象MakeCall
,也许是通过像深度克隆这样的机制。这样,我将能够验证MakeCall
使用我期望调用它的确切对象调用它。不幸的是,这不是我所看到的。
我试图在下面的代码中说明这一点(希望在这个过程中稍微澄清一下)。
- 我首先创建一个新
TestClass
对象。其Var
属性设置为"one"
。 - 然后我创建模拟对象
mockedObject
,这是我的测试对象。 - 然后我调用的
MakeCall
方法mockedObject
(顺便说一下,示例中使用的 Machine.Specifications 框架允许When_Testing
从上到下读取类中的代码)。 - 然后我测试模拟对象以确保它确实是
TestClass
用Var
值为 的 a 调用的"one"
。正如我所料,这成功了。 - 然后,我通过将属性
TestClass
重新分配给.Var
"two"
- 然后,我继续尝试验证 Moq 是否仍然认为使用值为 的 a
MakeCall
调用。这失败了,尽管我希望它是真的。TestClass
"one"
- 最后,我测试一下 Moq 是否认为
MakeCall
实际上是由TestClass
一个值为"two"
. 这成功了,尽管我最初预计它会失败。
对我来说似乎很清楚 Moq 仅保留对原始TestClass
对象的引用,允许我随意更改其值,从而对我的测试结果产生不利影响。
关于测试代码的一些注释。 IMyMockedInterface
是我正在嘲笑的界面。 TestClass
是我传递给MakeCall
方法的类,因此用来演示我遇到的问题。最后,When_Testing
是包含测试代码的实际测试类。它使用的是Machine.Specifications框架,这就是为什么会有一些奇怪的项目('因为','它应该......')。这些只是由框架调用以执行测试的委托。如果需要,它们应该很容易被删除,并将包含的代码放入标准函数中。我以这种格式保留它,因为它允许所有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 代码之一。希望它能起到澄清作用。
// 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
};