【问题标题】:Unexpected Validate behavior with MoqMoq 的意外验证行为
【发布时间】:2011-04-01 22:41:37
【问题描述】:

起订量让我对我的最新项目有点抓狂。我最近升级到版本 4.0.10827,我注意到在我看来这是一种新行为。

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

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

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

对我来说似乎很清楚 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
};

【问题讨论】:

    标签: asp.net-mvc moq mspec


    【解决方案1】:

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

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

    总结一下你遇到的问题:


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

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

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

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

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

    • 参考起订量的内存空间在其历史更改中保持为新值。

    • 然后您询问 Moq 是否曾使用不等于其持有的引用的对象调用该方法。

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


    尝试回答您的具体问题:

    1. 这是预期的行为吗?

      我会说不。

    2. 这是新行为吗?

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

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

      我会以两种方式回答。

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

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

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

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

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

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

    希望这会有所帮助,

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

    【讨论】:

    • 德里克 - 感谢您的回复。我绝对同意你的结论。实际上,在发布此内容后一段时间,我最终更改了被测代码。这些更改导致在初始调用数据库模拟之后创建了另一个变量,这缓解了我上面描述的问题。在我的脑海里,我仍然认为这是对通过值与引用传递对象的“新手”误解。很高兴看到其他人同意我对验证行为的期望是合理的。
    【解决方案2】:

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

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

    那么,当你想使用 Moq 的It 时,你只需要前缀Moq.,例如Moq.It.IsAny&lt;&gt;()


    关于您的问题。

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

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

    1. Because 委托正在运行
    2. It 代表一个接一个地运行。如果更改状态,则以下It 将永远不会看到 Because 中的设置。因此你的测试失败了。

    我已尝试使用 SetupForEachSpecificationAttribute 标记您的规范:

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

    该属性如其名称所述:它将运行您的EstablishBecause 在每个 It 之前。添加属性使规范的行为符合预期:3 次成功,1 次失败(Var = "two" 的验证)。

    SetupForEachSpecificationAttribute 会解决您的问题,还是会在您的测试不接受每个It 后重置?

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


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

    【讨论】:

    • 我想你可能误解了我的例子。我只提供了 MSpec 代码,因为它可以轻松运行以显示我在正常测试中看到的相同结果。正如您所知道的,我确实使用了建立委托,通常在一个抽象类中,但没有看到这个示例的必要性。我理解“It”委托的目的,但根据我的经验,它可以用来设置变量。我将尝试添加一些实际代码以进行澄清,但它无法运行。我很欣赏你关于“它”冲突的提示。我以后肯定会使用它。
    • @AwesomeYetIronicPseudonym - 啊,我明白了。看看你是否确实可以添加一些实际的代码,我会尝试再试一次:)
    • Sergi,我在附加代码中添加了。如果它澄清了一些事情,请告诉我。
    • @AwesomeYetIronicPseudonym - 我再次尝试回答。让我知道我们是否要去某个地方,或者我是否完全错过了这一点再次 :)
    • Sergi,我想我还是有误会。我的问题的目的不是解决我的代码本身的问题。我的代码完全按照我的预期运行。不过,我最近升级了 Moq,它表现出我不熟悉的行为。基本上,它允许我更改属于作为参数传递的对象的属性的值,并且只会针对更新的对象进行验证。我确实有工作起订量设置语句等,我只是没有包括它们。我提供了第一个示例作为测试代码供其他人运行以查看 Moq 行为。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-14
    • 1970-01-01
    • 1970-01-01
    • 2017-05-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多