使用中央 DI 逻辑进行单元测试和最小化 "Arrange" 阶段
Unit testing and minimizing "Arrange" phase with central DI logic
我的单元测试唯一的问题是我的 Arrange
阶段(在 Arrange-Act-Assert pattern
中)很长,因此出现了可维护性问题。我在测试 classes 的构造函数中进行了数十次注册。一些测试 classes 有共同的注册,我认为我可以将所有这些注册移动到一个中心位置并在其上使用 AssemblyInitialize
属性(例如,这种方法就像 Global.asax
或 Startup.cs
)。如果有人想覆盖注册的类型,它可以简单地重新注册它(在这种情况下,我的容器覆盖了旧的注册类型)。但是,一些测试也会有一些额外的注册 classes。
那会是个糟糕的决定吗?如果是这样,为什么?
是否有任何第三方工具可以帮助我(也许以更好的方式)?
这样做会影响我的单元测试时间吗?因为有些测试 classes 只有几个注册。我是否应该使用一些东西来为他们忽略这些中央注册?
编辑:
示例代码:
private CustomersController controller;
public CustomersControllerTests()
{
var container = ContainerFactory.GetContainer();
MapperConfiguration autoMapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile<DefaultMappingProfile>();
});
container.RegisterInstance(autoMapperConfig);
container.Register<Common.Contracts.Mapper.IMapper, AutoMapperWrapped>(Lifetime.Singleton);
container.Register<IProxy, BusinessProxy>();
container.Register<IDataContext, FakeDataContext>();
container.Register<IUnitOfWork, FakeUnitOfWork>();
container.Register<ICustomerBusiness, CustomerBusiness>();
//...Some more registrations
controller = new CustomersController();
}
[TestMethod]
public async Task GetCustomerById_ForNonExistingCustomer_ReturnsNotFound()
{
//Act
var result = await controller.GetCustomerById(0);
//Assert
Assert.IsInstanceOfType(result, typeof(NotFoundResult));
}
对于此测试方法,Arrange 部分位于测试的构造函数中 class,如图所示。
您的单元测试正在测试单个单元 - 即 class。理想情况下,任何依赖项都应该是模拟的,您可以完全控制从它们返回的内容。
SUT Builder 模式是一个值得遵循的好模式,尽管有些人不同意。 Mark Seemann 在 Pluralsight 上的高级单元测试课程中对此进行了精彩的解释 - 任何想要进行单元测试的人 - 我都会尝试将他们指向这门课程。
SUT Builder 模式是一种 Fluent API 模式,您基本上可以使用它来创建被测系统 (SUT) - 即,class 正在接受单元测试。这可以提高测试的可读性并保持干燥(不要重复自己)。
快速示例
internal sealed class MySutBuilder
{
private ISignInService _webServiceSignInService;
public MySutBuilder()
{
_webServiceSignInService = new Mock<ISignInService>().Object;
}
public MySutBuilder WithSecurityAccess()
{
var authenticationMock = new Mock<ISignInService>();
authenticationMock.Setup(m => m.SignIn(It.IsAny<GetDocumentContentRequest>()))
.Returns(SignInResult.Success(new ServiceUserContextMock()));
_webServiceSignInService = authenticationMock.Object;
return this;
}
public MyService Build()
{
MyService cut = new MyService(_webServiceSignInService);
return cut;
}
}
所以这个想法是在单元测试中使用 SutBuilder 来控制 class 的依赖关系。如果您向构造函数添加内容,那么您只需要在测试中的一个地方修复它 - 但我想这将取决于您的测试如何进行。
这是一段精简的代码,因此我们拥有的 classes 可以变得相当大,每个 class.
有 5 个以上的依赖项
单元测试用法
// arrange
var sut = new MySutBuilder()
.WithSecrityAccess()
.Build();
// act
var result = sut.DoSomething();
// assert
我的单元测试唯一的问题是我的 Arrange
阶段(在 Arrange-Act-Assert pattern
中)很长,因此出现了可维护性问题。我在测试 classes 的构造函数中进行了数十次注册。一些测试 classes 有共同的注册,我认为我可以将所有这些注册移动到一个中心位置并在其上使用 AssemblyInitialize
属性(例如,这种方法就像 Global.asax
或 Startup.cs
)。如果有人想覆盖注册的类型,它可以简单地重新注册它(在这种情况下,我的容器覆盖了旧的注册类型)。但是,一些测试也会有一些额外的注册 classes。
那会是个糟糕的决定吗?如果是这样,为什么?
是否有任何第三方工具可以帮助我(也许以更好的方式)?
这样做会影响我的单元测试时间吗?因为有些测试 classes 只有几个注册。我是否应该使用一些东西来为他们忽略这些中央注册?
编辑:
示例代码:
private CustomersController controller;
public CustomersControllerTests()
{
var container = ContainerFactory.GetContainer();
MapperConfiguration autoMapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile<DefaultMappingProfile>();
});
container.RegisterInstance(autoMapperConfig);
container.Register<Common.Contracts.Mapper.IMapper, AutoMapperWrapped>(Lifetime.Singleton);
container.Register<IProxy, BusinessProxy>();
container.Register<IDataContext, FakeDataContext>();
container.Register<IUnitOfWork, FakeUnitOfWork>();
container.Register<ICustomerBusiness, CustomerBusiness>();
//...Some more registrations
controller = new CustomersController();
}
[TestMethod]
public async Task GetCustomerById_ForNonExistingCustomer_ReturnsNotFound()
{
//Act
var result = await controller.GetCustomerById(0);
//Assert
Assert.IsInstanceOfType(result, typeof(NotFoundResult));
}
对于此测试方法,Arrange 部分位于测试的构造函数中 class,如图所示。
您的单元测试正在测试单个单元 - 即 class。理想情况下,任何依赖项都应该是模拟的,您可以完全控制从它们返回的内容。
SUT Builder 模式是一个值得遵循的好模式,尽管有些人不同意。 Mark Seemann 在 Pluralsight 上的高级单元测试课程中对此进行了精彩的解释 - 任何想要进行单元测试的人 - 我都会尝试将他们指向这门课程。
SUT Builder 模式是一种 Fluent API 模式,您基本上可以使用它来创建被测系统 (SUT) - 即,class 正在接受单元测试。这可以提高测试的可读性并保持干燥(不要重复自己)。
快速示例
internal sealed class MySutBuilder
{
private ISignInService _webServiceSignInService;
public MySutBuilder()
{
_webServiceSignInService = new Mock<ISignInService>().Object;
}
public MySutBuilder WithSecurityAccess()
{
var authenticationMock = new Mock<ISignInService>();
authenticationMock.Setup(m => m.SignIn(It.IsAny<GetDocumentContentRequest>()))
.Returns(SignInResult.Success(new ServiceUserContextMock()));
_webServiceSignInService = authenticationMock.Object;
return this;
}
public MyService Build()
{
MyService cut = new MyService(_webServiceSignInService);
return cut;
}
}
所以这个想法是在单元测试中使用 SutBuilder 来控制 class 的依赖关系。如果您向构造函数添加内容,那么您只需要在测试中的一个地方修复它 - 但我想这将取决于您的测试如何进行。
这是一段精简的代码,因此我们拥有的 classes 可以变得相当大,每个 class.
有 5 个以上的依赖项单元测试用法
// arrange
var sut = new MySutBuilder()
.WithSecrityAccess()
.Build();
// act
var result = sut.DoSomething();
// assert