我正在进行实例方法的单元测试。该方法恰好是ASP.NET MVC 4控制器操作,但我认为这并不重要。我们刚刚在这个方法中发现了一个错误,我想使用TDD修复bug并确保它不会回来。
测试中的方法调用返回对象的服务。然后它调用一个传递该对象的字符串属性的内部方法。该错误是在某些情况下,服务返回null,导致测试中的方法抛出NullReferenceException。
控制器使用依赖注入,因此我能够模拟服务客户端以使其返回null对象。问题是我想要更改测试中的方法,以便当服务返回null时,应该使用默认字符串值调用内部方法。
我能想到的唯一方法就是对被测试的类使用模拟。我希望能够断言,或者验证是否已使用正确的默认值调用此内部方法。当我尝试这个时,我得到一个MockException,声明调用没有在mock上执行。然而,我能够调试代码,并使用正确的参数查看调用的内部方法。
证明被测方法调用另一个传递特定参数值的方法的正确方法是什么?
答案 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();
}
}