23|DI Container(11):如何对ContainerTest进行文档化改造?
徐昊
你好,我是徐昊。今天我们继续使用 TDD 的方式实现注入依赖容器。
回顾测试代码与任务列表
上节课我们专注于测试代码的重构,目前我们的测试是这样的:
InjectTest:
package geektime.tdd.di;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class InjectionTest {
private Dependency dependency = mock(Dependency.class);
private Context context = mock(Context.class);
@BeforeEach
public void setup() {
when(context.get(eq(Dependency.class))).thenReturn(Optional.of(dependency));
}
@Nested
public class ConstructorInjection {
@Nested
class Injection {
static class DefaultConstructor {
}
@Test
public void should_call_default_constructor_if_no_inject_constructor() {
DefaultConstructor instance = new ConstructorInjectionProvider<>(DefaultConstructor.class).get(context);
assertNotNull(instance);
}
static class InjectConstructor {
Dependency dependency;
@Inject
public InjectConstructor(Dependency dependency) {
this.dependency = dependency;
}
}
@Test
public void should_inject_dependency_via_inject_constructor() {
InjectConstructor instance = new ConstructorInjectionProvider<>(InjectConstructor.class).get(context);
assertSame(dependency, instance.dependency);
}
@Test
public void should_include_dependency_from_inject_constructor() {
ConstructorInjectionProvider<InjectConstructor> provider = new ConstructorInjectionProvider<>(InjectConstructor.class);
assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}
}
@Nested
class IllegalInjectConstructors {
abstract class AbstractComponent implements Component {
@Inject
public AbstractComponent() {
}
}
@Test
public void should_throw_exception_if_component_is_abstract() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(AbstractComponent.class));
}
@Test
public void should_throw_exception_if_component_is_interface() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(Component.class));
}
static class MultiInjectConstructors {
@Inject
public MultiInjectConstructors(AnotherDependency dependency) {
}
@Inject
public MultiInjectConstructors(Dependency dependency) {
}
}
@Test
public void should_throw_exception_if_multi_inject_constructors_provided() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(MultiInjectConstructors.class));
}
static class NoInjectNorDefaultConstructor {
public NoInjectNorDefaultConstructor(Dependency dependency) {
}
}
@Test
public void should_throw_exception_if_no_inject_nor_default_constructor_provided() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(NoInjectNorDefaultConstructor.class));
}
}
}
@Nested
public class FieldInjection {
@Nested
class Injection {
static class ComponentWithFieldInjection {
@Inject
Dependency dependency;
}
static class SubclassWithFieldInjection extends ComponentWithFieldInjection {
}
@Test
public void should_inject_dependency_via_field() {
ComponentWithFieldInjection component = new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class).get(context);
assertSame(dependency, component.dependency);
}
@Test
public void should_inject_dependency_via_superclass_inject_field() {
SubclassWithFieldInjection component = new ConstructorInjectionProvider<>(SubclassWithFieldInjection.class).get(context);
assertSame(dependency, component.dependency);
}
@Test
public void should_include_dependency_from_field_dependency() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider = new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);
assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}
}
@Nested
class IllegalInjectFields {
static class FinalInjectField {
@Inject
final Dependency dependency = null;
}
@Test
public void should_throw_exception_if_inject_field_is_final() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(FinalInjectField.class));
}
}
}
@Nested
public class MethodInjection {
@Nested
class Injection {
static class InjectMethodWithNoDependency {
boolean called = false;
@Inject
void install() {
this.called = true;
}
}
@Test
public void should_call_inject_method_even_if_no_dependency_declared() {
InjectMethodWithNoDependency component = new ConstructorInjectionProvider<>(InjectMethodWithNoDependency.class).get(context);
assertTrue(component.called);
}
static class InjectMethodWithDependency {
Dependency dependency;
@Inject
void install(Dependency dependency) {
this.dependency = dependency;
}
}
@Test
public void should_inject_dependency_via_inject_method() {
InjectMethodWithDependency component = new ConstructorInjectionProvider<>(InjectMethodWithDependency.class).get(context);
assertSame(dependency, component.dependency);
}
static class SuperClassWithInjectMethod {
int superCalled = 0;
@Inject
void install() {
superCalled++;
}
}
static class SubclassWithInjectMethod extends SuperClassWithInjectMethod {
int subCalled = 0;
@Inject
void installAnother() {
subCalled = superCalled + 1;
}
}
@Test
public void should_inject_dependencies_via_inject_method_from_superclass() {
SubclassWithInjectMethod component = new ConstructorInjectionProvider<>(SubclassWithInjectMethod.class).get(context);
assertEquals(1, component.superCalled);
assertEquals(2, component.subCalled);
}
static class SubclassOverrideSuperClassWithInject extends SuperClassWithInjectMethod {
@Inject
void install() {
super.install();
}
}
@Test
public void should_only_call_once_if_subclass_override_inject_method_with_inject() {
SubclassOverrideSuperClassWithInject component = new ConstructorInjectionProvider<>(SubclassOverrideSuperClassWithInject.class).get(context);
assertEquals(1, component.superCalled);
}
static class SubclassOverrideSuperClassWithNoInject extends SuperClassWithInjectMethod {
void install() {
super.install();
}
}
@Test
public void should_not_call_inject_method_if_override_with_no_inject() {
SubclassOverrideSuperClassWithNoInject component = new ConstructorInjectionProvider<>(SubclassOverrideSuperClassWithNoInject.class).get(context);
assertEquals(0, component.superCalled);
}
@Test
public void should_include_dependencies_from_inject_method() {
ConstructorInjectionProvider<InjectMethodWithDependency> provider = new ConstructorInjectionProvider<>(InjectMethodWithDependency.class);
assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}
}
@Nested
class IllegalInjectMethods {
static class InjectMethodWithTypeParameter {
@Inject
<T> void install() {
}
}
@Test
public void should_throw_exception_if_inject_method_has_type_parameter() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(InjectMethodWithTypeParameter.class));
}
}
}
}
待重构的 ContainerTest 是这样的:
package geektime.tdd.di;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.internal.util.collections.Sets;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
public class ContainerTest {
ContextConfig config;
@BeforeEach
public void setup() {
config = new ContextConfig();
}
@Nested
public class ComponentConstruction {
@Test
public void should_bind_type_to_a_specific_instance() {
Component instance = new Component() {
};
config.bind(Component.class, instance);
Context context = config.getContext();
assertSame(instance, context.get(Component.class).get());
}
@Test
public void should_return_empty_if_component_not_defined() {
Optional<Component> component = config.getContext().get(Component.class);
assertTrue(component.isEmpty());
}
@Nested
public class DependencyCheck {
@Test
public void should_throw_exception_if_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> config.getContext());
assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}
@Test
public void should_throw_exception_if_cyclic_dependencies_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnComponent.class);
CyclicDependenciesFoundException exception = assertThrows(CyclicDependenciesFoundException.class, () -> config.getContext());
Set<Class<?>> classes = Sets.newSet(exception.getComponents());
assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}
@Test
public void should_throw_exception_if_transitive_cyclic_dependencies_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
config.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);
CyclicDependenciesFoundException exception = assertThrows(CyclicDependenciesFoundException.class, () -> config.getContext());
List<Class<?>> components = Arrays.asList(exception.getComponents());
assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}
}
}
@Nested
public class DependenciesSelection {
}
@Nested
public class LifecycleManagement {
}
}
interface Component {
}
interface Dependency {
}
interface AnotherDependency {
}
class ComponentWithInjectConstructor implements Component {
private Dependency dependency;
@Inject
public ComponentWithInjectConstructor(Dependency dependency) {
this.dependency = dependency;
}
public Dependency getDependency() {
return dependency;
}
}
class DependencyDependedOnComponent implements Dependency {
private Component component;
@Inject
public DependencyDependedOnComponent(Component component) {
this.component = component;
}
}
class AnotherDependencyDependedOnComponent implements AnotherDependency {
private Component component;
@Inject
public AnotherDependencyDependedOnComponent(Component component) {
this.component = component;
}
}
class DependencyDependedOnAnotherDependency implements Dependency {
private AnotherDependency anotherDependency;
@Inject
public DependencyDependedOnAnotherDependency(AnotherDependency anotherDependency) {
this.anotherDependency = anotherDependency;
}
}
任务列表没有改变,目前的状态为:
无需构造的组件——组件实例
如果注册的组件不可实例化,则抛出异常
抽象类
接口
构造函数注入
无依赖的组件应该通过默认构造函数生成组件实例
有依赖的组件,通过 Inject 标注的构造函数生成组件实例
如果所依赖的组件也存在依赖,那么需要对所依赖的组件也完成依赖注入
如果组件有多于一个 Inject 标注的构造函数,则抛出异常
如果组件没有 Inject 标注的构造函数,也没有默认构造函数(新增任务)
如果组件需要的依赖不存在,则抛出异常
如果组件间存在循环依赖,则抛出异常
字段注入
通过 Inject 标注将字段声明为依赖组件
如果字段为 final 则抛出异常
依赖中应包含 Inject Field 声明的依赖
方法注入
通过 Inject 标注的方法,其参数为依赖组件
通过 Inject 标注的无参数方法,会被调用
按照子类中的规则,覆盖父类中的 Inject 方法
如果方法定义类型参数,则抛出异常
依赖中应包含 Inject Method 声明的依赖
对 Provider 类型的依赖
注入构造函数中可以声明对于 Provider 的依赖
注入字段中可以声明对于 Provider 的依赖
注入方法中可声明对于 Provider 的依赖
自定义 Qualifier 的依赖
注册组件时,可额外指定 Qualifier
注册组件时,可从类对象上提取 Qualifier
寻找依赖时,需同时满足类型与自定义 Qualifier 标注
支持默认 Qualifier——Named
Singleton 生命周期
注册组件时,可额外指定是否为 Singleton
注册组件时,可从类对象上提取 Singleton 标注
对于包含 Singleton 标注的组件,在容器范围内提供唯一实例
容器组件默认不是 Single 生命周期
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 英语
- 中文简体
- 中文繁体
- 法语
- 德语
- 日语
- 韩语
- 俄语
- 西班牙语
- 阿拉伯语
- 解释
- 总结
本文介绍了徐八叉关于DI容器的系列文章中的第11篇,重点讲解了如何对ContainerTest进行文档化改造。文章首先回顾了测试代码与任务列表,然后展示了待重构的ContainerTest代码。接着列出了任务列表,包括组件构造、依赖检查、依赖选择、生命周期管理等多个方面的任务。文章还提出了思考题,邀请读者分享实现剩余任务的想法和项目代码链接。整体来看,本文通过具体的代码示例和任务列表,深入讲解了DI容器的实现细节,适合对DI容器感兴趣的读者学习参考。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
立即购买
登录 后留言
全部留言(6)
- 最新
- 精选
- 人间四月天非常感谢,讲解让工程师可以写出高质量的代码,测试驱动,测试驱动设计,让中国工程师摆脱curd,容器的例子很好,需求明确,需求有复杂性,测试如何驱动功能实现,保证代码的正确性,设计的合理性。有个问题,先实现原型功能,我认为没问题,可是对于复杂需求,是不是要模块化设计一下,把职责非常明确的类和方法先设计好,然后再结合经典和伦敦两种学派,更高效?如何用老师的方法,都是发现类的职责不单一了,然后再重构,为什么不能开始就想到,设计好?例如spring容器,注册和构建组件,本身就是很复杂的,为什么最早就把构建和使用分离?
作者回复: 重构能力越强 设计越可以后置
2022-05-0326 - keep_curiosityClass.getMethods() 方法好像默认就包含了子类覆盖父类方法的逻辑,可以省掉自己过滤的逻辑。测试也都没问题。
作者回复: 只对public
2022-05-04 - 张铁林23敲好的代码 https://github.com/vfbiby/tdd-di-container
编辑回复: nice!
2022-05-042 - tdd学徒ContainerTest 文档化之后62个测试能对得上 Component应该要加下面一点修改 interface Component { default Dependency dependency() { return null; } }2022-05-0411
- 人间四月天内部类不能有静态的声明,老师为什么不报错,typebinding本身是内部类,他的成员有静态的内部类2022-05-291
- aoe收获: 经过梳理之后得到可执行的测试文档 通过抽取方法起到注释的作用2022-05-05
收起评论