徐昊 · TDD 项目实战 70 讲
徐昊
Thoughtworks 中国区 CTO
18159 人已学习
新⼈⾸单¥98
登录后,你可以任选4讲全文学习
课程目录
已完结/共 88 讲
实战项目二|RESTful开发框架:依赖注入容器 (24讲)
实战项目三|RESTful Web Services (44讲)
徐昊 · TDD 项目实战 70 讲
15
15
1.0x
00:00/00:00
登录|注册

21|DI Container(9):怎样重构测试代码?

你好,我是徐昊。今天我们继续使用 TDD 的方式实现注入依赖容器。

回顾代码与任务列表

到目前为止,我们的代码是这样的:
ContextConfig.java:
package geektime.tdd.di;
import java.util.*;
import static java.util.List.of;
public class ContextConfig {
private Map<Class<?>, ComponentProvider<?>> providers = new HashMap<>();
public <Type> void bind(Class<Type> type, Type instance) {
providers.put(type, new ComponentProvider<Type>() {
@Override
public Type get(Context context) {
return instance;
}
@Override
public List<Class<?>> getDependencies() {
return of();
}
});
}
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
providers.put(type, new ConstructorInjectionProvider<>(implementation));
}
public Context getContext() {
providers.keySet().forEach(component -> checkDependencies(component, new Stack<>()));
return new Context() {
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type) provider.get(this));
}
};
}
private void checkDependencies(Class<?> component, Stack<Class<?>> visiting) {
for (Class<?> dependency: providers.get(component).getDependencies()) {
if (!providers.containsKey(dependency)) throw new DependencyNotFoundException(component, dependency);
if (visiting.contains(dependency)) throw new CyclicDependenciesFoundException(visiting);
visiting.push(dependency);
checkDependencies(dependency, visiting);
visiting.pop();
}
}
interface ComponentProvider<T> {
T get(Context context);
List<Class<?>> getDependencies();
}
}
ConstructorInjectionProvider.java:
package geektime.tdd.di;
import jakarta.inject.Inject;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.stream.Stream.concat;
class ConstructorInjectionProvider<T> implements ContextConfig.ComponentProvider<T> {
private Constructor<T> injectConstructor;
private List<Field> injectFields;
private List<Method> injectMethods;
public ConstructorInjectionProvider(Class<T> component) {
this.injectConstructor = getInjectConstructor(component);
this.injectFields = getInjectFields(component);
this.injectMethods = getInjectMethods(component);
}
@Override
public T get(Context context) {
try {
Object[] dependencies = stream(injectConstructor.getParameters())
.map(p -> context.get(p.getType()).get())
.toArray(Object[]::new);
T instance = injectConstructor.newInstance(dependencies);
for (Field field : injectFields)
field.set(instance, context.get(field.getType()).get());
for (Method method : injectMethods)
method.invoke(instance, stream(method.getParameterTypes()).map(t -> context.get(t).get())
.toArray(Object[]::new));
return instance;
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Class<?>> getDependencies() {
return concat(concat(stream(injectConstructor.getParameters()).map(Parameter::getType),
injectFields.stream().map(Field::getType)),
injectMethods.stream().flatMap(m -> stream(m.getParameterTypes()))
).toList();
}
private static <T> List<Method> getInjectMethods(Class<T> component) {
List<Method> injectMethods = new ArrayList<>();
Class<?> current = component;
while(current != Object.class) {
injectMethods.addAll(stream(current.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(Inject.class))
.filter(m -> injectMethods.stream().noneMatch(o -> o.getName().equals(m.getName()) &&
Arrays.equals(o.getParameterTypes(), m.getParameterTypes())))
.filter(m -> stream(component.getDeclaredMethods()).filter(m1 -> !m1.isAnnotationPresent(Inject.class))
.noneMatch(o -> o.getName().equals(m.getName()) &&
Arrays.equals(o.getParameterTypes(), m.getParameterTypes())))
.toList());
current = current.getSuperclass();
}
Collections.reverse(injectMethods);
return injectMethods;
}
private static <T> List<Field> getInjectFields(Class<T> component) {
List<Field> injectFields = new ArrayList<>();
Class<?> current = component;
while (current != Object.class) {
injectFields.addAll(stream(current.getDeclaredFields()).filter(f -> f.isAnnotationPresent(Inject.class))
.toList());
current = current.getSuperclass();
}
return injectFields;
}
private static <Type> Constructor<Type> getInjectConstructor(Class<Type> implementation) {
List<Constructor<?>> injectConstructors = stream(implementation.getConstructors())
.filter(c -> c.isAnnotationPresent(Inject.class)).collect(Collectors.toList());
if (injectConstructors.size() > 1) throw new IllegalComponentException();
return (Constructor<Type>) injectConstructors.stream().findFirst().orElseGet(() -> {
try {
return implementation.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalComponentException();
}
});
}
}
Context.java:
package geektime.tdd.di;
import java.util.Optional;
public interface Context {
<Type> Optional<Type> get(Class<Type> type);
}
测试代码为:
package geektime.tdd.di;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.annotation.Testable;
import org.mockito.Mockito;
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 ConstructorInjection {
//TODO: abstract class
//TODO: interface
@Test
public void should_bind_type_to_a_class_with_default_constructor() {
config.bind(Component.class, ComponentWithDefaultConstructor.class);
Component instance = config.getContext().get(Component.class).get();
assertNotNull(instance);
assertTrue(instance instanceof ComponentWithDefaultConstructor);
}
@Test
public void should_bind_type_to_a_class_with_inject_constructor() {
Dependency dependency = new Dependency() {
};
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, dependency);
Component instance = config.getContext().get(Component.class).get();
assertNotNull(instance);
assertSame(dependency, ((ComponentWithInjectConstructor) instance).getDependency());
}
@Test
public void should_bind_type_to_a_class_with_transitive_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyWithInjectConstructor.class);
config.bind(String.class, "indirect dependency");
Component instance = config.getContext().get(Component.class).get();
assertNotNull(instance);
Dependency dependency = ((ComponentWithInjectConstructor) instance).getDependency();
assertNotNull(dependency);
assertEquals("indirect dependency", ((DependencyWithInjectConstructor) dependency).getDependency());
}
@Test
public void should_throw_exception_if_multi_inject_constructors_provided() {
assertThrows(IllegalComponentException.class, () -> {
config.bind(Component.class, ComponentWithMultiInjectConstructors.class);
});
}
@Test
public void should_throw_exception_if_no_inject_nor_default_constructor_provided() {
assertThrows(IllegalComponentException.class, () -> {
config.bind(Component.class, ComponentWithNoInjectConstructorNorDefaultConstructor.class);
});
}
@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_transitive_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyWithInjectConstructor.class);
DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> config.getContext());
assertEquals(String.class, exception.getDependency());
assertEquals(Dependency.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 FieldInjection {
static class ComponentWithFieldInjection {
@Inject
Dependency dependency;
}
static class SubclassWithFieldInjection extends ComponentWithFieldInjection {
}
@Test
public void should_inject_dependency_via_field() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);
ComponentWithFieldInjection component = config.getContext().get(ComponentWithFieldInjection.class).get();
assertSame(dependency, component.dependency);
}
@Test
public void should_inject_dependency_via_superclass_inject_field() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(SubclassWithFieldInjection.class, SubclassWithFieldInjection.class);
SubclassWithFieldInjection component = config.getContext().get(SubclassWithFieldInjection.class).get();
assertSame(dependency, component.dependency);
}
//TODO throw exception if field is final
@Test
public void should_include_field_dependency_in_dependencies() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider = new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);
assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}
}
@Nested
public class MethodInjection {
static class InjectMethodWithNoDependency {
boolean called = false;
@Inject
void install() {
this.called = true;
}
}
@Test
public void should_call_inject_method_even_if_no_dependency_declared() {
config.bind(InjectMethodWithNoDependency.class, InjectMethodWithNoDependency.class);
InjectMethodWithNoDependency component = config.getContext().get(InjectMethodWithNoDependency.class).get();
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() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(InjectMethodWithDependency.class, InjectMethodWithDependency.class);
InjectMethodWithDependency component = config.getContext().get(InjectMethodWithDependency.class).get();
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() {
config.bind(SubclassWithInjectMethod.class, SubclassWithInjectMethod.class);
SubclassWithInjectMethod component = config.getContext().get(SubclassWithInjectMethod.class).get();
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() {
config.bind(SubclassOverrideSuperClassWithInject.class, SubclassOverrideSuperClassWithInject.class);
SubclassOverrideSuperClassWithInject component = config.getContext().get(SubclassOverrideSuperClassWithInject.class).get();
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() {
config.bind(SubclassOverrideSuperClassWithNoInject.class, SubclassOverrideSuperClassWithNoInject.class);
SubclassOverrideSuperClassWithNoInject component = config.getContext().get(SubclassOverrideSuperClassWithNoInject.class).get();
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));
}
//TODO throw exception if type parameter defined
}
}
@Nested
public class DependenciesSelection {
}
@Nested
public class LifecycleManagement {
}
}
interface Component {
}
interface Dependency {
}
interface AnotherDependency {
}
class ComponentWithDefaultConstructor implements Component {
public ComponentWithDefaultConstructor() {
}
}
class ComponentWithInjectConstructor implements Component {
private Dependency dependency;
@Inject
public ComponentWithInjectConstructor(Dependency dependency) {
this.dependency = dependency;
}
public Dependency getDependency() {
return dependency;
}
}
class ComponentWithMultiInjectConstructors implements Component {
@Inject
public ComponentWithMultiInjectConstructors(String name, Double value) {
}
@Inject
public ComponentWithMultiInjectConstructors(String name) {
}
}
class ComponentWithNoInjectConstructorNorDefaultConstructor implements Component {
public ComponentWithNoInjectConstructorNorDefaultConstructor(String name) {
}
}
class DependencyWithInjectConstructor implements Dependency {
private String dependency;
@Inject
public DependencyWithInjectConstructor(String dependency) {
this.dependency = dependency;
}
public String 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 标注的回调
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

徐八叉的文章《DI Container(9):怎样重构测试代码?》深入探讨了利用TDD的方式来实现注入依赖容器的方法和技巧。通过详细介绍绑定类型、处理多个注入构造函数和无注入构造函数的情况,以及字段注入、方法注入、Provider类型的依赖和自定义Qualifier的依赖等内容,作者生动展示了如何利用TDD的方式来重构测试代码,以及在实现注入依赖容器时的一些技术特点。文章通过代码示例和测试用例,为想深入了解TDD和依赖注入的读者提供了很高的参考价值。文章内容涵盖了TDD和依赖注入的重要技术特点,对于想深入了解这些主题的读者具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《徐昊 · TDD 项目实战 70 讲》
新⼈⾸单¥98
立即购买
登录 后留言

全部留言(5)

  • 最新
  • 精选
  • Flynn
    DI这个项目会在下个项目用上不

    作者回复: 回用

    2022-04-28
  • aoe
    - 通过 TDD 获得的测试,可以驱动我们的开发,但不代表获得的是一个良好的 Test Case 组合 - TDD 主要是为我们开发生产代码提供驱动力 - 天然得出的结果并不能认为是很好的 Test Case - 所以需要对 Test Case 进行重构 - 消除在构造 TDD 过程中留下的不一样的印记(架构选择、设计决策等) - 使 Test Case 能真实反应代码的意图 - 按测试意图将零散的测试方法收集到一起(放入同一个 Nested 中或者单独的测试类中) - 同一个上下文中,测试粒度尽量保持一致 - 清理没有用的测试
    2022-05-03
    2
  • 老师您好,请问为什么把测试类往外面移的时候,要先把它声明为static?
    2023-03-14归属地:广东
  • davix
    才知道TDD寫過的cases要重新組織,之前看到的TDD介紹太淺,都未提過。 請教老師,developer寫的test cases的好標準是啥?有延展閱讀嗎?
    2022-05-25
  • 枫中的刀剑
    本篇总结: 测试重构的目的:让TDD的 Test Case 更好的反映出我们代码的意图,而不仅仅是单纯展示实现功能的过程。 测试代码的坏味道: 主要体现在「不一致」。 1. 设计决策变化导致的测试冗余。 2. 同类型功能不同架构选择导致的不一致。(同一类功能测试中包含不同上下文) 3. 不同功能,相似结构的测试中表现的不一致。其中某些特有功能可能不在属于当前上下文。(这种情况稍微难发现一些)。 体会:测试的重构也很重要,结构优良的Test Case 更加清晰地呈现实现代码的真实意图。
    2022-05-02
收起评论
显示
设置
留言
5
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部