Moq的意外验证行为

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

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

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

  • 我首先创建一个新的TestClass对象。 其Var属性设置为"one"
  • 然后我创建了mockedObject对象mockedObject ,它是我的测试主题。
  • 然后我调用MakeCall的方法mockedObject (顺便说一下,在该示例中使用的Machine.Specifications框架允许在代码When_Testing从顶部读至底部类)。
  • 然后测试模拟对象以确保它确实使用Var值为"one"TestClass调用。 正如我预料的那样,这成功了。
  • 然后我通过重新分配Var属性为"two"来对原始TestClass对象进行更改。
  • 然后,我继续尝试验证Moq是否仍然认为MakeCall是使用值为"one"TestClass调用的。 这失败了,虽然我期待它是真实的。
  • 最后,我测试一下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());
    }
    

    我有几个关于这个问题的问题:

    这是预期的行为?
    这是新的行为吗?
    有没有我不知道的解决方法?
    我是否使用验证不正确?
    有没有更好的方式使用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
    };
    

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

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

    总结您遇到的问题:


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

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

  • 然后你问Moq,如果方法被调用一次,其中一个对象等于你传入的引用。

  • Moq使用与提供的参数相匹配的参数来检查其历史记录以调用该方法,并回答yes。

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

  • 参考Moq的记忆空间正在其历史变化中维持新的价值。

  • 然后你问Moq,如果该方法被调用一次,而该对象不等于它所持有的引用。

  • 模拟用一个匹配提供的参数和报告编号的参数来检查其历史记录以调用该方法。


  • 尝试回答您的具体问题:

  • 这是预期的行为?

    我会说不。

  • 这是新的行为吗?

    我不知道,但是这个项目可能会有一些行为促进了这一点,并且后来被修改为只允许只验证一次模拟使用的简单场景。

  • 有没有我不知道的解决方法?

    我会回答这两个方面。

    从技术角度来看,解决方法是使用Test Spy而不是Mock。 通过使用Test Spy,您可以记录传递的值并使用自己的策略来记住状态,例如执行深层克隆,序列化对象,或仅存储您关心的与以后进行比较的特定值。

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

  • 我是否使用验证不正确?

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

  • 有没有更好的方式使用Moq来避免这种情况?

    我不认为目前正在实施Moq来处理这种情况。

  • 希望这可以帮助,

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


    首先,你可以通过声明来避免MoqMSpec之间的冲突

    using Machine.Specifications;
    using Moq;
    using It = Machine.Specifications.It;
    

    那么你只需要用Moq.前缀Moq. 当你想使用起订量的It ,例如Moq.It.IsAny<>()


    在你的问题上。

    注意:这不是原始答案,而是在OP向问题添加了一些实际示例代码之后编辑的答案

    我一直在试用你的示例代码,我认为MSpec比Moq更重要。 显然,(我不知道这不是),当你修改里面的SUT(待测系统)的状态, It代表的变化被记住。 现在发生的事情是:

  • Because委托运行
  • It代表是一个接一个地运行的。 如果改变状态,下面It永远不会看到在Because的设置。 因此你的失败测试。
  • 我试着用SetupForEachSpecificationAttribute标记你的规范:

    [Subject(typeof(object)), SetupForEachSpecification]
    public class When_Testing
    {
        // Something, Something, something... 
    }
    

    该属性的名称如下所示:它会在每个It之前运行您的EstablishBecause 。 添加属性使得规范按预期行事:3成功,一次失败(验证Var =“2”)。

    请问SetupForEachSpecificationAttribute解决您的问题或复位后每It为你的测试不能接受?

    FYI:我使用Moq v4.0.10827.0MSpec v0.4.9.0


    免费提示#2:如果您使用Mspec测试ASP.NET MVC应用程序,则可能需要查看James Broome的MVC扩展程序MVC

    链接地址: http://www.djcxy.com/p/6019.html

    上一篇: Unexpected Validate behavior with Moq

    下一篇: Accessing a global static variable from another class