应用 DI 时,您将依赖项的创建推迟到最后负责任的时刻。这意味着您可以尽可能长时间地承担创建依赖项的负担。但在应用程序的某个地方,需要创建这些依赖项。
这个依赖组合的地方称为成分根。对于您正在运行的应用程序,组合根可能是应用程序的Main
方法,或者至少在靠近应用程序启动路径的地方。
单元测试中的组合
编写单元测试时,每个单元测试本身都充当组合根。这意味着单元测试本身(或其调用的方法)本身负责它需要测试的类的组成。这意味着作文不再被推迟,也不再被推高。这就是您想要做的:将对象组合的责任推给单元测试框架。
尽管从技术上讲,某些单元测试框架允许您拦截创建测试类的方式,但让测试框架提供依赖项通常没有什么意义,因为单元测试本身需要控制正在创建的确切依赖项。测试不仅知道什么确切的类型依赖项应该是(即通常是某种假实现),但还需要配置这些(假)依赖项或查询它们的结果以断言测试的正确性。
这意味着不要尝试注入依赖项AgentProvisioningServiceHelpher
在构造函数内部UnitTest1
, the SimpleMethodToTest_Shall_ReturnPlus1
方法必须受控。例如:
[Fact]
public void SimpleMethodToTest_Shall_ReturnPlus1()
{
// Arrange
int input = 1;
int expectedResult = 2;
var sut = new AgentProvisioningServiceHelpher(
new FakeExcelParser(),
new FakeSupervisorDbContext(),
new FakeSchedulerNoTrackingDbContext());
// Act
var actualResult = sut.SimpleMethodToTest(input);
// Assert
Assert.Equal(expectedResult, actualResult);
}
不幸的是,随着编写的测试越多,在每个单元测试中创建被测类的所有依赖项变得越来越难以维护。在这种情况下,从测试中提取此组合逻辑到辅助方法或辅助类中就成为一个好习惯。在这种情况下,技巧是确保测试仅提供对该特定测试特别感兴趣的依赖项,而将其余部分留空。例如:
[Fact]
public void Parser_should_always_be_called()
{
// Arrange
var parser = new FakeExcelParser();
AgentProvisioningServiceHelpher sut = this.CreateSut(excelParser: parser);
// Act
sut.SimpleMethodToTest(0);
// Assert
Assert.IsTrue(parser.GotCalled);
}
private AgentProvisioningServiceHelpher CreateSut(
IExcelParser excelParser = null,
SupervisorDbContext supervisorDbContext = null,
SchedulerNoTrackingDbContext schedulerDbContext = null)
{
return new AgentProvisioningServiceHelpher(
excelParser ?? new FakeExcelParser(),
supervisorDbContext ?? new FakeSupervisorDbContext(),
schedulerDbContext ?? new FakeSchedulerNoTrackingDbContext());
}
在本次测试中,仅ExcelParser
已提供,因为在测试期间显式查询了它。其他两个依赖项将由默认(可能是假的)空实现提供CreateSut
method.
在这种情况下,CreateSut
成为组合根的一部分。
集成测试中的组合
在编写单元测试时,依赖关系通常是手动连接的,如上所示。但是,如果您正在编写集成测试,则测试中涉及的对象数量通常会更大,并且需要类似于生产应用程序中组成的对象结构(有时需要替换一些依赖项)。让单个测试方法或测试类手动重新创建完整的对象结构通常很麻烦,而且容易出错。应用程序对象结构的更改可能会通过许多测试,并且很容易导致系统的维护性很差。
相反,在集成测试期间,通常尝试重用正在运行的应用程序的组合根使用的相同对象组合逻辑。当您使用 DI 容器来组成应用程序的对象图时,这通常意味着重复使用那些相同的 DI 容器注册。
集成测试将重用相同的 DI 容器配置,模拟集成测试运行所需的一些依赖项,解析被测类并调用其方法之一。但是,集成测试仍然无法从外部注入这些依赖项,因为它可能需要对创建的内容进行一些控制。集成测试仍然是它自己的组合根,尽管它将部分对象组合委托给单独的 Composer 类(DI 容器)。
这是集成测试的示例:
[Fact]
public void Some_integration_test()
{
// Arrange
int input = 1;
int expectedResult = 2;
// Mock object
var parser = new FakeExcelParser();
// Create a valid container to resolve object graphs from
var container = TestBootstrapper.BuildContainer();
// Configure it especially for this test (note that I'm inventing a
// DI Container API here. API will very per DI Container)
container.Replace<IExcelParser>(parser);
// Resolve the SUT from the DI Container
var sut = container.Resolve<AgentProvisioningServiceHelpher>();
// Act
var actualResult = sut.SimpleMethodToTest(input);
// Assert
Assert.Equal(expectedResult, actualResult);
}
此集成测试使用TestBootstrapper
可以在集成测试之间共享的类:
public static class TestBootstrapper
{
public static Container BuildContainer()
{
// Request a fully configured DI Container instance from the
// actual application. This ensures that the integration test
// runs using the exact same object graphs as the final application.
var container = RealApplication.Bootstrapper.BuildContainer();
// Replace dependencies that should never be used during the
// integration tests.
container.Replace<IHardDiskFormatter, FakeDiskFormatter>();
container.Replace<ISmsSender, FakeSmsSender>();
container.Replace<IPaymentProvider, FakePaymentProvider>();
return container;
}
}
这当然与单元测试有很大不同,单元测试具有高度的隔离性。