Moq的意外验证行为

Awe*_*nym 7 asp.net-mvc moq mspec

在我的最新项目中,Moq让我有点疯狂.我最近升级到版本4.0.10827,我注意到在我看来是一个新的行为.

基本上,当我MakeCall在我正在测试的代码中调用我的模拟函数(在本例中)时,我传入一个object(TestClass).我正在测试的代码TestClass在调用之前和之后对对象进行了更改MakeCall.一旦代码完成,我就调用Moq的Verify功能.我的期望是,Moq将记录我传入的完整对象MakeCall,可能通过深度克隆等机制.通过这种方式,我将能够验证是否MakeCall使用我希望调用它的确切对象进行调用.不幸的是,这不是我所看到的.

我试图在下面的代码中说明这一点(希望在此过程中澄清一点).

  1. 我首先创建一个新TestClass对象.它的Var财产设置为"one".
  2. 然后我创建了模拟对象mockedObject,这是我的测试主题.
  3. 然后我调用MakeCall方法mockedObject(顺便说一下,示例中使用的Machine.Specifications框架允许When_Testing从上到下读取类中的代码).
  4. 然后我测试模拟对象,以确保它的确TestClass使用Var值为"one".正如我所预料的那样,这成功了.
  5. 然后我TestClass通过重新分配Var属性来更改原始对象"two".
  6. 然后我继续尝试验证Moq是否仍然认为使用值为的MakeCall调用.这失败了,虽然我期待它是真的. TestClass"one"
  7. 最后,我测试看看Moq是否认为MakeCall实际上是由TestClass具有值的对象调用的"two".这成功了,虽然我最初预计它会失败.

对我来说,似乎很清楚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());
}
Run Code Online (Sandbox Code Playgroud)

我有几个问题:

这是预期的行为吗?
这是新的行为吗?
有没有我不知道的解决方法?
我错误地使用了验证吗?
有没有更好的方法使用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
};
Run Code Online (Sandbox Code Playgroud)

Der*_*eer 3

最终,您的问题是模拟框架是否应该拍摄您在与模拟交互时使用的参数的快照,以便它可以准确地记录系统在交互时所处的状态,而不是参数在交互时可能所处的状态。验证点。

我想说,从逻辑的角度来看,这是一个合理的期望。您正在执行具有值 Y 的操作 X。如果您询问模拟“我是否执行了具有值 Y 的操作 X”,您希望它说“是”,无论系统的当前状态如何。

总结一下您遇到的问题:


  • 您首先使用引用类型参数调用模拟对象上的方法。

  • Moq 保存有关调用的信息以及传入的引用类型参数。

  • 然后,您询问 Moq 是否使用等于您传入的引用的对象调用了该方法一次。

  • Moq 检查其对该方法的调用历史记录,该方法的参数与所提供的参数相匹配,并回答“是”。

  • 然后,您可以修改作为参数传递给模拟上的方法调用的对象。

  • 参考最小起订量的内存空间在其历史记录中保存着新值的更改。

  • 然后,您询问 Moq 是否使用一个不等于其所持有的引用的对象调用了该方法一次。

  • Mock 检查其调用该方法的历史记录,该方法的参数与所提供的参数匹配,并报告否。


尝试回答您的具体问题:

  1. 这是预期的行为吗?

    我会说不。

  2. 这是新行为吗?

    我不知道,但值得怀疑的是,该项目曾经有过促进这一点的行为,后来被修改为只允许每个模拟仅验证一次使用的简单场景。

  3. 有我不知道的解决方法吗?

    我会从两个方面来回答这个问题。

    从技术角度来看,解决方法是使用测试间谍而不是模拟。通过使用测试间谍,您可以记录传递的值并使用您自己的策略来记住状态,例如进行深度克隆、序列化对象,或者仅存储您关心的特定值以便稍后进行比较。

    从测试的角度来看,我建议您遵循“首先使用前门”的原则。我相信有时会进行基于状态的测试以及基于交互的测试,但您应该尽量避免将自己与实现细节耦合,除非交互是场景的重要组成部分。在某些情况下,您感兴趣的场景主要是关于交互(“在帐户之间转移资金”),但在其他情况下,您真正​​关心的是获得正确的结果(“提取 10 美元”)。就控制器的规范而言,这似乎属于查询类别,而不是命令类别。你并不真正关心它如何得到你想要的结果,只要它们是正确的。因此,我建议在这种情况下使用基于状态的测试。如果另一个规范涉及针对系统发出命令,则最终可能仍然存在您应该首先考虑使用的前门解决方案,但进行基于交互的测试可能是必要或重要的。只是我的想法。

  4. 我是否错误地使用了验证?

    您正确使用了Verify()方法,它只是不支持您使用它的场景。

  5. 有没有更好的方法使用 Moq 来避免这种情况?

    我认为目前还没有实现最小起订量来处理这种情况。

希望这可以帮助,

德里克·格里尔
http://derekgreer.lostechies.com
http://aspiringcraftsman.com
@derekgreer