徐昊 · 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 impleme