跳转至

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标注的回调

视频演示

让我们进入今天的部分:

思考题

剩余任务在现有代码结构下,要如何实现?

欢迎把你的想法分享在留言区,也欢迎把你的项目代码的链接分享出来。相信经过你的思考与实操,学习效果会更好!

精选留言(6)
  • 人间四月天 👍(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