如何使用Moq证明正在测试的方法调用另一个方法

时间:2014-11-15 05:36:44

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

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

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

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

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

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

2 个答案:

答案 0 :(得分: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);
    }
}

编辑:根据评论中的建议使用“验证”而不是回调。 验证参数值的更简单方法是在执行测试系统后使用严格的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();
    }

答案 1 :(得分:1)

&#34;我能想到的唯一方法就是在测试中使用模拟器。&#34;

我认为你不应该模拟测试中的课程。模拟测试中的类只有外部依赖。你可以做的是创建一个testable-class。它将是一个派生自您的CUT的类,您可以在此处捕获another method的调用并稍后验证它的参数。 HTH

  • 示例中的可测试类名为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();
    }
}