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

22|DI Container(10):怎样将大粒度的测试重构为等效的小粒度测试代码?

你好,我是徐昊。今天我们继续使用 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) {
if (Modifier.isAbstract(component.getModifiers())) throw new IllegalComponentException();
this.injectConstructor = getInjectConstructor(component);
this.injectFields = getInjectFields(component);
this.injectMethods = getInjectMethods(component);
if (injectFields.stream().anyMatch(f -> Modifier.isFinal(f.getModifiers()))) throw new IllegalComponentException();
if (injectMethods.stream().anyMatch(m -> m.getTypeParameters().length != 0)) throw new IllegalComponentException();
}
@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);
}
任务列表状态为:
无需构造的组件——组件实例
如果注册的组件不可实例化,则抛出异常
抽象类
接口
构造函数注入
无依赖的组件应该通过默认构造函数生成组件实例
有依赖的组件,通过 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
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了如何将大粒度的测试重构为等效的小粒度测试代码,通过使用TDD的方式实现注入依赖容器。文章首先回顾了代码与任务列表,然后展示了如何使用TDD的方式实现注入依赖容器。接着提出了两个思考题,鼓励读者思考并分享他们的想法。整篇文章围绕着如何使用TDD的方式实现注入依赖容器展开,通过代码示例和任务列表的状态,帮助读者了解了如何进行代码重构和测试。读者可以通过本文了解到如何使用TDD的方式实现注入依赖容器,以及在重构过程中使用的重构手法。

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

全部留言(6)

  • 最新
  • 精选
  • 张铁林
    置顶
    https://github.com/vfbiby/tdd-di-container/tree/main/doc 在这下面有每一章的“心法”,照着练,就不用看视频了,全程尽量跟着老师的步骤来。
    2022-04-29
    1
  • aoe
    测试变文档 - 从文档角度优化测试 - 使用 @Nested 将功能分组 - 测试天然不是文档,而是你实现过程的记录 - 对测试进行足够提取和刻意的组织后才能变成真正的文档
    2022-05-04
    1
  • 临风
    代码坏味道就是你看了代码觉得不好理解的地方,本质就是为了提高代码的可维护性和可读性。之前我写代码的时候容易考虑很多,导致代码过度设计。后面学习代码重构手法之后,配合上TDD的使用,就有充足信心,减少甚至不考虑未来的拓展性问题。因为将来代码改动时,你可以通过重构的手法,使其适应改动的方向,再进行代码功能的增添,使代码能一直保持很高的可读性。 另外对于测试代码的重构,老师的“测试天然表现的是开发实现的逻辑,测试天然不是文档”的观点让我印象深刻。之前也听过,你不要让别人通过逐行看代码来理解代码逻辑,而是要通过测试用例来告诉别人你的代码逻辑和功能,直到完成测试的重构才彻底明白了这一观点的由来。 上次作业完成的时候,和老师的实现略有不同,经过老师的提示后,进行了重构。期间充分体会到TDD的好处,由于测试的充分覆盖,你的每个功能点都不用害怕代码的重构会不小心改坏了。看着简洁的代码,竟莫名有点小成就感。 最后有个问题想问老师,这个container是因为只有自己一个人写,可以随心所欲。实际项目中,如何才能在缺少测试用例的情况下进行一定的代码重构呢?老师会建议直接复用代码,还是有其它好的方法呢? https://github.com/lenwind/TDD-Learn
    2022-05-02
    1
  • 奇小易
    Q: 如何发现坏味道? 本文出现两个坏味道, 一个是一个测试类的上下文中存在粒度(测试范围)不同的测试。 另一个是测试本身不具有文档的性质,不好理解。 据此可知,好的测试需要具备文档化的特点,好的测试需要在同一上下文保持一致的粒度,保持一致的命名思路。 这些良好测试的特点就是用于识别坏味道的指导方针。
    2022-06-03
  • davix
    老師可否講講測試粒度的選擇、組織?TDD適不適合測試金字塔各個層? 縱然大粒度測試應該少,但前提是大粒度測試的低效(如難維護的依賴,易出錯等)。如果像本項目這樣的純內存裡的測試,大粒度不是更合適嗎?更有利於內部的重構。
    2022-05-25
  • 张铁林
    https://github.com/vfbiby/tdd-di-container 我来贴一下代码,没有每一步小的做提交,主要是为了方便回滚到上一课,再来练习。
    2022-04-29
收起评论
显示
设置
留言
6
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部