7

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

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

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

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

2 回答 2

3

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

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

总结您遇到的问题:


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

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

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

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

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

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

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

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


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

  1. 这是预期的行为吗?

    我会说不。

  2. 这是新行为吗?

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

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

    我会以两种方式回答。

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

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

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

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

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

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

希望这可以帮助,

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

于 2011-04-09T00:11:53.713 回答
0

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

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. 因此,您的测试失败。

我试过用以下标记你的规格SetupForEachSpecificationAttribute

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

Establish该属性就像它的名字所说的那样:它将Because 在每个 It. 添加属性使规范按预期运行:3 次成功,1 次失败(Var = "two" 的验证)。

SetupForEachSpecificationAttribute解决您的问题还是在It您的测试不接受之后重新设置?

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


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

于 2011-04-04T09:14:28.170 回答