我最近开始对c#代码进行单元测试但是之前我从未测试过控制器类。我有以下课程,我需要为此编写测试用例。
namespace nH.MasterData.API.Controllers
{
[Produces("application/json")]
[Route("api/AType")]
public class ATypeController : Controller
{
private readonly IProcessor _domain;
private readonly ILogger _logger;
private const string Version = "VERSION_1";
private const string Model = "model";
private const string Entity = "AType";
public ATypeController(IProxy proxy, ILogger<ATypeController> logger)
{
_logger = logger;
var proxy = proxy.GetProxy();
_domain = new InstitutionAddressProcessor(proxy);
}
[HttpGet]
public OkObjectResult Get()
{
try
{
return Ok(_domain.GetATypesMembers(Model, Version, Entity, MemberType.Leaf, null));
}
catch (Exception ex)
{
_logger.LogError(ex.Message + " " + ex.StackTrace);
return new OkObjectResult(BadRequest());
}
}
}
}
在这里,_domain.GetATypesMembers()
不在我的项目中,我没有明确地创建_domain
的对象(它在构造函数中创建)。如何控制在构造函数中创建的对象?因此,我可以在调用时模拟响应而不实际进行实际调用。
通常,我会setup
对象的模拟作为wcfMockService.Setup(x => x.GetATypesMembers(...).ReturnAsync(response);
但我怎么能写这个?
我很感激任何建议。
Edit1:在构造函数中创建对象是不好的做法。但这是一个遗留代码,并在许多地方使用。所以我只是在代码存在时尝试编写测试。
您亲身经历的是为什么将您的课程与实施问题紧密结合是一种糟糕的设计原因。
应该重构该控制器以依赖于抽象而不是结构。
namespace nH.MasterData.API.Controllers {
[Produces("application/json")]
[Route("api/AType")]
public class ATypeController : Controller {
private readonly IProcessor _domain;
private readonly ILogger _logger;
private const string Version = "VERSION_1";
private const string Model = "model";
private const string Entity = "AType";
public ATypeController(IProcessor domain, ILogger<ATypeController> logger) {
_logger = logger;
_domain = domain;
}
[HttpGet]
public IActionResult Get() {
try {
return Ok(_domain.GetATypesMembers(Model, Version, Entity, MemberType.Leaf, null));
} catch (Exception ex) {
_logger.LogError(ex.Message + " " + ex.StackTrace);
return BadRequest();
}
}
}
}
现在可以在测试时根据需要模拟依赖项
var processor = new Mock<IProcessor>();
var logger = new Mock<ILogger<ATypeController>>();
var controller = new ATypeController(processor.Object, logger.Object);
//...setup mocks
从原始构造函数的外观看起来应该相应地配置IProcessor
以满足其依赖性,因为它的构造函数不是真实的,它需要在注入之前进行中间调用。
这可以在组合根处得到满足。
services.AddScoped<IProcessor>(_ => {
var proxy = _.GetService<IProxy>();
return new InstitutionAddressProcessor(proxy.GetProxy());
});
如果我理解你的情况,你无法纠正构造函数使用正确的DI模型,你无法解决这个问题。假设你有一个有效的IProxy
实例,你可以注入以正确地满足对象构造,你可以转向一些脏的反射黑客来让你的IProcessor
模拟。
在你构建你的sut之后,你可以设置一些东西。
// Arrange
var sut = SutProvider.GetATypeController(); // A system under test factory.
var mock = new Mock<IProcessor>();
// ... mock setup ..
typeof(ATypeController)
.GetField("_domain", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(sut, mock.Object);
// Act
var result = sut.Get();
// Assert
// ... assertions of result
这不是一个理想的设置,但在处理遗留代码时,有时您必须做脏事来设置对象的正确状态以进行测试。我强烈建议您将这些内容仅保留在测试中,并在“实际”代码中对此进行适当的评论。
SutProvider是一个简单的工厂,实现可以如下所示:
public class SutProvider
{
public static ATypeController GetATypeController() => new ATypeController(GetProxy(), GetATypeControllerLogger());
public static IProxy GetProxy() {
// Either return a valid IProxy, or set up a mock that can return a result from the GetProxy method that is valid enough to withstand InstitutionAddressProcessor's constructor.
}
public static ILogger<ATypeController> GetATypeControllerLogger() => new Mock<ILogger<ATypeController>>().Object;
}
您可能需要深入了解InstitutionAddressProcessor
的构造函数,以找出传递足够有效代理的最佳方法。
我将使用Dependency Injection就是这种情况。它是Inversion Control的一种形式。我不会在方法中传递代理和_domain,而是将其作为参数传递(代理正在传递,但我会将_domain作为参数传递)。 _domain将在Main like method
的某处实例化,或者可以使用['AutoFac']
。然后,创建单元测试变得容易。
这是使用Dependency Injection测试控制器的有用链接
如上所述,用户指出,存在紧密耦合,这使得难以进行单元测试。
你没有得到任何免费的东西。如果你想能够测试逻辑,你必须解耦。如果你不解耦它们,你将无法正确测试,因为你正在测试一些你自己无法测试的依赖项。所以没有“单位”。
虽然说它可能是遗留代码但是像resharper这样的合适工具,我与之无关,可以帮助你进行重构。注入服务后,您就可以进行适当的单元测试。