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生命周期
自定义Scope标注
- 可向容器注册自定义Scope标注的回调
视频演示
让我们进入今天的部分:
思考题
剩余任务在现有代码结构下,要如何实现?
欢迎把你的想法分享在留言区,也欢迎把你的项目代码的链接分享出来。相信经过你的思考与实操,学习效果会更好!
- 人间四月天 👍(7) 💬(2)
非常感谢,讲解让工程师可以写出高质量的代码,测试驱动,测试驱动设计,让中国工程师摆脱curd,容器的例子很好,需求明确,需求有复杂性,测试如何驱动功能实现,保证代码的正确性,设计的合理性。有个问题,先实现原型功能,我认为没问题,可是对于复杂需求,是不是要模块化设计一下,把职责非常明确的类和方法先设计好,然后再结合经典和伦敦两种学派,更高效?如何用老师的方法,都是发现类的职责不单一了,然后再重构,为什么不能开始就想到,设计好?例如spring容器,注册和构建组件,本身就是很复杂的,为什么最早就把构建和使用分离?
2022-05-03 - 张铁林 👍(1) 💬(3)
23敲好的代码 https://github.com/vfbiby/tdd-di-container
2022-05-04 - keep_curiosity 👍(0) 💬(1)
Class.getMethods() 方法好像默认就包含了子类覆盖父类方法的逻辑,可以省掉自己过滤的逻辑。测试也都没问题。
2022-05-04 - tdd学徒 👍(1) 💬(1)
ContainerTest 文档化之后62个测试能对得上 Component应该要加下面一点修改 interface Component { default Dependency dependency() { return null; } }
2022-05-04 - 人间四月天 👍(0) 💬(1)
内部类不能有静态的声明,老师为什么不报错,typebinding本身是内部类,他的成员有静态的内部类
2022-05-29 - aoe 👍(0) 💬(0)
收获: 经过梳理之后得到可执行的测试文档 通过抽取方法起到注释的作用
2022-05-05