提问人:John Saunders 提问时间:11/15/2014 更新时间:11/20/2014 访问量:752
如何使用最小起订量来证明被测方法调用另一个方法
How to use Moq to Prove that the Method under test Calls another Method
问:
我正在对实例方法进行单元测试。该方法恰好是 MVC 4 控制器操作 ASP.NET,但我认为这并不重要。我们刚刚在这个方法中发现了一个错误,我想使用 TDD 来修复这个错误并确保它不会再次出现。
所测试的方法调用返回对象的服务。然后,它调用一个内部方法,传递此对象的字符串属性。该错误是,在某些情况下,服务返回 null,导致受测方法引发 NullReferenceException。
控制器使用依赖注入,因此我已经能够模拟服务客户端以使其返回 null 对象。问题是我想更改所测试的方法,以便当服务返回 null 时,应使用默认字符串值调用内部方法。
我能想到的唯一方法是对被测类使用模拟。我希望能够断言或验证是否已使用正确的默认值调用此内部方法。当我尝试这样做时,我得到一个 MockException,指出调用未在模拟上执行。然而,我能够调试代码并看到使用正确的参数调用的内部方法。
证明被测方法调用另一个传递特定参数值的方法的正确方法是什么?
答:
“我能想到的唯一方法是对被测班级使用模拟。”
我认为你不应该模拟考试中的课程。仅模拟被测类所具有的外部依赖项。您可以做的是创建一个 .它将是一个派生自您的 CUT 的类,在这里您可以捕获对 的调用并稍后验证其参数。它)testable-class
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();
}
}
评论
我认为这里有一种代码味道。在这种情况下,我会问自己的第一个问题是,“内部”方法是否真的是被测控制器的内部/私有方法。执行“内部”任务是控制者的责任吗?当内部方法的实现发生更改时,控制器是否应该更改?可能不是。
在这种情况下,我会拉出一个新的目标类,它有一个公共方法,可以执行迄今为止控制器内部的操作。 完成此重构后,我将使用 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();
}
评论