Moq的意外验证行为

时间:2011-04-01 22:41:38

标签: asp.net-mvc moq mspec

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

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

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

  1. 我首先创建一个新的TestClass对象。其Var属性设置为"one"
  2. 然后我创建了模拟对象mockedObject,这是我的测试主题。
  3. 然后我调用MakeCall的{​​{1}}方法(顺便说一下,示例中使用的Machine.Specifications框架允许从mockedObject类中读取代码从顶部到底部)。
  4. 然后,我测试模拟对象以确保使用When_Testing TestClassVar确实调用它。正如我所预料的那样,这成功了。
  5. 然后,我通过将"one"属性重新分配给TestClass来对原始Var对象进行更改。
  6. 然后,我继续尝试验证Moq是否仍然认为使用"two"调用了MakeCall且值为TestClass。这失败了,虽然我期待它是真的。
  7. 最后,我测试看看Moq是否认为"one"实际上是由MakeCall对象调用的,其值为TestClass。这成功了,虽然我最初预计它会失败。
  8. 我觉得很清楚,Moq只保留对原始"two"对象的引用,允许我改变其值而不受惩罚,对我的测试结果产生不利影响。

    关于测试代码的一些注释。 TestClass是我嘲笑的界面。 IMyMockedInterface是我传递给TestClass方法的类,因此用于演示我遇到的问题。最后,MakeCall是包含测试代码的实际测试类。它使用Machine.Specifications框架,这就是为什么有一些奇怪的项目('因为','它应该...')。这些只是框架调用以执行测试的委托。如果需要,应该很容易删除它们并将包含的代码放入标准函数中。我把它保留为这种格式,因为它允许所有When_Testing次调用完成(与'Arrange,Act Assert'范式相比)。只是为了澄清,下面的代码不是我遇到问题的实际代码。它只是为了说明问题,因为我在多个地方看到了同样的行为。

    Validate

    我对此有几个问题:

    这是预期的行为吗? 这是新的行为吗? 有没有我不知道的解决方法?
    我是否错误地使用了验证? 有没有更好的方法使用Moq来避免这种情况?

    我谦卑地感谢你提供的任何帮助。

    修改
    这是我遇到此问题的实际测试和SUT代码之一。希望它将作为澄清。

    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());
    }
    

2 个答案:

答案 0 :(得分:3)

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

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

总结您遇到的问题:


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

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

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

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

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

  • 参考Moq的内存空间在其历史记录中保持更改为新值。

  • 然后你问Moq是否一次调用该方法的对象不等于它所持有的引用。

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


尝试回答您的具体问题:

  1. 这是预期的行为吗?

    我会说不。

  2. 这是新行为吗?

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

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

    我会回答这两种方式。

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

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

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

    您正在使用Verify()方法,它只是不支持您使用它的方案。

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

    我认为目前没有实施Moq来处理这种情况。

  6. 希望这有帮助,

    Derek Greer
    http://derekgreer.lostechies.com
    http://aspiringcraftsman.com
    @derekgreer

答案 1 :(得分:0)

首先,您可以通过声明

来避免MoqMSpec之间的冲突
using Machine.Specifications;
using Moq;
using It = Machine.Specifications.It;

然后,当您想要使用Moq的Moq.时,您只需要使用It作为前缀,例如Moq.It.IsAny<>()


在你的问题上。

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

我一直在尝试你的示例代码,我认为它与MSpec有关,而不是Moq。显然(我也不知道这一点),当您在It委托中修改SUT(受测试系统)的状态时,会记住更改。现在发生的事情是:

  1. Because委托正在运行
  2. It代表一个接一个地运行。如果更改状态,则以下It 将永远不会在 Because中看到设置。因此你的考试失败了。
  3. 我尝试使用SetupForEachSpecificationAttribute标记您的规范:

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

    该属性的名称如下:它将在每个 Establish之前运行您的BecauseIt 。添加属性使得规范符合预期:3个成功,一个失败(使用Var =“2”的验证)。

    SetupForEachSpecificationAttribute是否可以解决您的问题,或者在您的测试无法接受的每个It之后重置?

    仅供参考:我正在使用Moq v4.0.10827.0MSpec v0.4.9.0


    免费提示#2:如果您正在使用Mspec测试ASP.NET MVC应用程序,您可能需要查看James Broome's MSpec extensions for MVC