如何使用最小起订量来证明被测方法调用另一个方法

How to use Moq to Prove that the Method under test Calls another Method

提问人:John Saunders 提问时间:11/15/2014 更新时间:11/20/2014 访问量:752

问:

我正在对实例方法进行单元测试。该方法恰好是 MVC 4 控制器操作 ASP.NET,但我认为这并不重要。我们刚刚在这个方法中发现了一个错误,我想使用 TDD 来修复这个错误并确保它不会再次出现。

所测试的方法调用返回对象的服务。然后,它调用一个内部方法,传递此对象的字符串属性。该错误是,在某些情况下,服务返回 null,导致受测方法引发 NullReferenceException。

控制器使用依赖注入,因此我已经能够模拟服务客户端以使其返回 null 对象。问题是我想更改所测试的方法,以便当服务返回 null 时,应使用默认字符串值调用内部方法。

我能想到的唯一方法是对被测类使用模拟。我希望能够断言或验证是否已使用正确的默认值调用此内部方法。当我尝试这样做时,我得到一个 MockException,指出调用未在模拟上执行。然而,我能够调试代码并看到使用正确的参数调用的内部方法。

证明被测方法调用另一个传递特定参数值的方法的正确方法是什么?

单元测试 visual-studio-2013 最小起订量

评论

0赞 Sam Holder 11/15/2014
内部方法的调用是否没有可观察到的行为?你到底想验证什么?该方法是使用预期的默认值调用的,或者只是在服务返回 null 时它不会爆炸?其中第一个对我来说似乎很脆弱,因为如果有人更改默认值,它就会失败。如果这很重要,那么很公平,但是您真的关心默认值是否已更改,或者只是在服务返回null时该方法不会失败吗?
0赞 Patrick Quirk 11/15/2014
你能展示一些伪代码吗?这听起来像是一个常见的模拟场景,但如果没有一些代码,我不能肯定地说。
0赞 John Saunders 11/15/2014
@SamHolder:内部方法根据此参数的值返回具有一到三个值的集合。不幸的是,它不是一对一的,所以我不能使用返回值来确定传递了哪个参数。我将在几个小时内用伪代码扩展这个问题。

答:

1赞 Daniel Dušek 11/15/2014 #1

“我能想到的唯一方法是对被测班级使用模拟。”

我认为你不应该模拟考试中的课程。仅模拟被测类所具有的外部依赖项。您可以做的是创建一个 .它将是一个派生自您的 CUT 的类,在这里您可以捕获对 的调用并稍后验证其参数。它)testable-classanother method

  • 示例中的可测试类被命名为MyTestableController
  • 另一种方法被命名为 。InternalMethod

简短示例:

[TestClass]
public class Tests
{
    [TestMethod]
    public void MethodUnderTest_WhenServiceReturnsNull_CallsInternalMethodWithDefault()
    {
        // Arrange
        Mock<IService> serviceStub = new Mock<IService>();
        serviceStub.Setup(s => s.ServiceCall()).Returns((ReturnedFromService)null);
        MyTestableController testedController = new MyTestableController(serviceStub.Object)
        {
            FakeInternalMethod = true
        };

        // Act
        testedController.MethodUnderTest();

        // Assert
        Assert.AreEqual(testedController.SomeDefaultValue, testedController.FakeInternalMethodWasCalledWithThisParameter);
    }

    private class MyTestableController
        : MyController
    {

        public bool FakeInternalMethod { get; set; }
        public string FakeInternalMethodWasCalledWithThisParameter { get; set; }

        public MyTestableController(IService service) 
            : base(service)
        { }

        internal override void InternalMethod(string someProperty)
        {
            if (FakeInternalMethod)
                FakeInternalMethodWasCalledWithThisParameter = someProperty;
            else
                base.InternalMethod(someProperty);
        }
    }
}

CUT 可能如下所示:

public class MyController : Controller
{
    private readonly IService _service;

    public MyController(IService service)
    {
        _service = service;
    }

    public virtual string SomeDefaultValue { get { return "SomeDefaultValue"; }}

    public EmptyResult MethodUnderTest()
    {
        // We just found a bug in this method ...

        // The method under test calls a service which returns an object.
        ReturnedFromService fromService = _service.ServiceCall();

        // It then calls an internal method passing a string property of this object
        string someStringProperty = fromService == null 
            ? SomeDefaultValue 
            : fromService.SomeProperty;
        InternalMethod(someStringProperty);

        return new EmptyResult();
    }

    internal virtual void InternalMethod(string someProperty)
    {
        throw new NotImplementedException();
    }
}

评论

0赞 Amol 11/18/2014
从长远来看,复制控制器代码可能会变得棘手。这种方法肯定会解决手头的问题,但很快就会变成一种反模式。思潮?
2赞 Amol 11/18/2014 #2

我认为这里有一种代码味道。在这种情况下,我会问自己的第一个问题是,“内部”方法是否真的是被测控制器的内部/私有方法。执行“内部”任务是控制者的责任吗?当内部方法的实现发生更改时,控制器是否应该更改?可能不是。

在这种情况下,我会拉出一个新的目标类,它有一个公共方法,可以执行迄今为止控制器内部的操作。 完成此重构后,我将使用 MOQ 的回调机制并断言参数值。

所以最终,你最终会嘲笑两个依赖关系: 1. 对外服务 2. 具有控制器内部实现的新目标类

现在,您的控制器是完全隔离的,可以独立进行单元测试。此外,“内部”实现变得可进行单元测试,并且也应该有自己的一组单元测试。

因此,您的代码和测试将如下所示:

public class ControllerUnderTest
{

    private IExternalService Service { get; set; }
    private NewFocusedClass NewFocusedClass { get; set; }
    const string DefaultValue = "DefaultValue";

    public ControllerUnderTest(IExternalService service, NewFocusedClass newFocusedClass)
    {
        Service = service;
        NewFocusedClass = newFocusedClass;
    }

    public void MethodUnderTest()
    {
        var returnedValue = Service.ExternalMethod();
        string valueToBePassed;
        if (returnedValue == null)
        {
            valueToBePassed = DefaultValue;
        }
        else
        {
            valueToBePassed = returnedValue.StringProperty;
        }
        NewFocusedClass.FocusedBehvaior(valueToBePassed);
    }
}

public interface IExternalService
{
    ReturnClass ExternalMethod();
}

public class NewFocusedClass
{
    public virtual void FocusedBehvaior(string param)
    {

    }
}

public class ReturnClass
{
    public string StringProperty { get; set; }
}

[TestClass]
public class ControllerTests
{
    [TestMethod]
    public void TestMethod()
    {
        //Given
        var mockService = new Mock<IExternalService>();
        mockService.Setup(s => s.ExternalMethod()).Returns((ReturnClass)null);
        var mockFocusedClass = new Mock<NewFocusedClass>();
        var actualParam = string.Empty;
        mockFocusedClass.Setup(x => x.FocusedBehvaior(It.IsAny<string>())).Callback<string>(param => actualParam = param);

        //when
        var controller = new ControllerUnderTest(mockService.Object, mockFocusedClass.Object);
        controller.MethodUnderTest();

        //then
        Assert.AreEqual("DefaultValue", actualParam);
    }
}

编辑:根据评论中的建议,使用“验证”而不是回调。 验证参数值的更简单方法是使用严格的最小起订量行为,并在执行受测系统后对模拟进行验证调用。 修改后的测试可能如下所示:

[TestMethod]
    public void TestMethod()
    {
        //Given
        var mockService = new Mock<IExternalService>();
        mockService.Setup(s => s.ExternalMethod()).Returns((ReturnClass)null);
        var mockFocusedClass = new Mock<NewFocusedClass>(MockBehavior.Strict);
        mockFocusedClass.Setup(x => x.FocusedBehvaior(It.Is<string>(s => s == "DefaultValue")));

        //When
        var controller = new ControllerUnderTest(mockService.Object, mockFocusedClass.Object);
        controller.MethodUnderTest();

        //Then
        mockFocusedClass.Verify();
    }

评论

1赞 d.Siva 11/19/2014
您还可以使用 .验证是否使用确切的参数调用方法。