【问题标题】:How to use Moq to Prove that the Method under test Calls another Method如何使用 Moq 证明被测方法调用了另一个方法
【发布时间】:2014-11-20 12:31:57
【问题描述】:

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

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

控制器使用依赖注入,所以我可以模拟服务客户端让它返回一个空对象。问题是我想更改被测方法,以便当服务返回 null 时,应该使用默认字符串值调用内部方法。

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

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

【问题讨论】:

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

标签: unit-testing visual-studio-2013 moq


【解决方案1】:

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

在这种情况下,我会提取一个新的目标类,它有一个公共方法,该方法执行迄今为止在控制器内部的事情。 有了这个重构,我将使用 MOQ 的 callback 机制并断言参数值。

所以最终,您将模拟两个依赖项: 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);
    }
}

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

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

【讨论】:

  • 您也可以使用 .Verify 来查看是否使用精确的参数调用方法。
【解决方案2】:

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

我认为你不应该模拟被测类。仅模拟您的测试类具有的外部依赖项。你可以做的是创建一个testable-class。这将是一个派生自 CUT 的类,在这里您可以捕获对 another 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();
    }
}

【讨论】:

  • 从长远来看,复制控制器代码会变得很棘手。这种方法肯定会解决手头的问题,但很快就会成为一种反模式。想法?
猜你喜欢
  • 1970-01-01
  • 2022-10-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-12
相关资源
最近更新 更多