小架构step系列13:测试用例的加载

1 概述

测试用例的编写要有一些基础的规范,在本文先定义文件名称和测试用例方法名的规范。

2 文件加载原理

先从源码来看一下测试用例的文件加载原理。

2.1 文件的匹配

主要是通过注解来扫描测试用例。

java 复制代码
// 在IDEA测试用例启动时,调用junit-platform-launcher-x.x.x.jar包里的类启动,最终会调JupiterTestEngine.discover()来加载测试用例
// 估计在不同的地方,启动的方式可能有所不同,junit-platform-launcher-x.x.x.jar包并没有引入到工程里
// 源码位置:org.junit.jupiter.engine.JupiterTestEngine
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
    JupiterConfiguration configuration = new CachingJupiterConfiguration(
        new DefaultJupiterConfiguration(discoveryRequest.getConfigurationParameters()));
    JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration);
    
    // 1. 先在DiscoverySelectorResolver()初始化resolver,然后使用resolver去加载测试用例
    new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor);
    return engineDescriptor;
}

// 源码位置:org.junit.jupiter.engine.discovery.DiscoverySelectorResolver
public class DiscoverySelectorResolver {
    // 初始化多个resolver
    private static final EngineDiscoveryRequestResolver<JupiterEngineDescriptor> resolver = EngineDiscoveryRequestResolver.<JupiterEngineDescriptor>builder()
            // 2. 设置加载测试用例文件的ClassContainerSelectorResolver,IsTestClassWithTests是最终的过滤类ClassFilter
            .addClassContainerSelectorResolver(new IsTestClassWithTests()) 
            .addSelectorResolver(context -> new ClassSelectorResolver(context.getClassNameFilter(), context.getEngineDescriptor().getConfiguration()))
            .addSelectorResolver(context -> new MethodSelectorResolver(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> new ClassOrderingVisitor(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> new MethodOrderingVisitor(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> TestDescriptor::prune)
            .build();

    public void resolveSelectors(EngineDiscoveryRequest request, JupiterEngineDescriptor engineDescriptor) {
        resolver.resolve(request, engineDescriptor);
    }
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.Builder
public Builder<T> addClassContainerSelectorResolver(Predicate<Class<?>> classFilter) {
    Preconditions.notNull(classFilter, "classFilter must not be null");
    // 3. 把ClassFilter包到ClassContainerSelectorResolver里面,这个ClassFilter是IsTestClassWithTests
    return addSelectorResolver(context -> new ClassContainerSelectorResolver(classFilter, context.getClassNameFilter()));
}
public Builder<T> addSelectorResolver(Function<InitializationContext<T>, SelectorResolver> resolverCreator) {
    // 4. ClassContainerSelectorResolver最终存储在resolverCreators变量里
    resolverCreators.add(resolverCreator);
    return this;
}

// 回到DiscoverySelectorResolver,调用resolveSelectors()
// 源码位置:org.junit.jupiter.engine.discovery.DiscoverySelectorResolver
public class DiscoverySelectorResolver {
    // 初始化多个resolver
    private static final EngineDiscoveryRequestResolver<JupiterEngineDescriptor> resolver = EngineDiscoveryRequestResolver.<JupiterEngineDescriptor>builder()
            // 2. 设置加载测试用例文件的ClassContainerSelectorResolver,IsTestClassWithTests是最终的过滤类ClassFilter
            .addClassContainerSelectorResolver(new IsTestClassWithTests()) 
            .addSelectorResolver(context -> new ClassSelectorResolver(context.getClassNameFilter(), context.getEngineDescriptor().getConfiguration()))
            .addSelectorResolver(context -> new MethodSelectorResolver(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> new ClassOrderingVisitor(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> new MethodOrderingVisitor(context.getEngineDescriptor().getConfiguration()))
            .addTestDescriptorVisitor(context -> TestDescriptor::prune)
            .build();

    public void resolveSelectors(EngineDiscoveryRequest request, JupiterEngineDescriptor engineDescriptor) {
        5. 执行resolver.resolve加载测试用例
        resolver.resolve(request, engineDescriptor);
    }
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver
public void resolve(EngineDiscoveryRequest request, T engineDescriptor) {
    Preconditions.notNull(request, "request must not be null");
    Preconditions.notNull(engineDescriptor, "engineDescriptor must not be null");
    InitializationContext<T> initializationContext = new DefaultInitializationContext<>(request, engineDescriptor);
    
    // 6. 初始化resolver,把resolverCreators作为参数传入
    List<SelectorResolver> resolvers = instantiate(resolverCreators, initializationContext);
    List<TestDescriptor.Visitor> visitors = instantiate(visitorCreators, initializationContext);
    new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors).run();
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver
private <R> List<R> instantiate(List<Function<InitializationContext<T>, R>> creators, InitializationContext<T> context) {
    // 7. 遍历Resolver执行lambda表达式函数,这个函数执行的时候,就是执行Resolver的resolve接口,
    //    其中重点在于ClassContainerSelectorResolver,里面放了IsTestClassWithTests这个ClassFilter,通过该类进行过滤
    return creators.stream().map(creator -> creator.apply(context)).collect(toCollection(ArrayList::new));
}

// 回到EngineDiscoveryRequestResolver
// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver
public void resolve(EngineDiscoveryRequest request, T engineDescriptor) {
    Preconditions.notNull(request, "request must not be null");
    Preconditions.notNull(engineDescriptor, "engineDescriptor must not be null");
    InitializationContext<T> initializationContext = new DefaultInitializationContext<>(request, engineDescriptor);
    // 6. 初始化resolver,把resolverCreators作为参数传入
    List<SelectorResolver> resolvers = instantiate(resolverCreators, initializationContext);
    List<TestDescriptor.Visitor> visitors = instantiate(visitorCreators, initializationContext);
    
    // 8. 执行resolver
    new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors).run();
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution
void run() {
    remainingSelectors.addAll(request.getSelectorsByType(DiscoverySelector.class));
    while (!remainingSelectors.isEmpty()) {
        // 9. 执行,参数为ClasspathRootSelector,里面放了测试用例class文件的根目录,如果是IDEA环境则是target/test-classes目录
        resolveCompletely(remainingSelectors.poll());
    }
    visitors.forEach(engineDescriptor::accept);
}
private void resolveCompletely(DiscoverySelector selector) {
    EngineDiscoveryListener discoveryListener = request.getDiscoveryListener();
    UniqueId engineId = engineDescriptor.getUniqueId();
    try {
        // 10. 执行,参数为ClasspathRootSelector
        Optional<Resolution> result = resolve(selector);
        if (result.isPresent()) {
            discoveryListener.selectorProcessed(engineId, selector, resolved());
            enqueueAdditionalSelectors(result.get());
        }
        else {
            discoveryListener.selectorProcessed(engineId, selector, unresolved());
        }
    }
    catch (Throwable t) {
        UnrecoverableExceptions.rethrowIfUnrecoverable(t);
        discoveryListener.selectorProcessed(engineId, selector, failed(t));
    }
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution
private Optional<Resolution> resolve(DiscoverySelector selector) {
    if (resolvedSelectors.containsKey(selector)) {
        return Optional.of(resolvedSelectors.get(selector));
    }
    if (selector instanceof UniqueIdSelector) {
        return resolveUniqueId((UniqueIdSelector) selector);
    }
    // 11. 执行resolver,重点关注ClassContainerSelectorResolver
    return resolve(selector, resolver -> {
        Context context = getContext(selector);
        if (selector instanceof ClasspathResourceSelector) {
            return resolver.resolve((ClasspathResourceSelector) selector, context);
        }
        if (selector instanceof ClasspathRootSelector) {
            return resolver.resolve((ClasspathRootSelector) selector, context);
        }
        if (selector instanceof ClassSelector) {
            return resolver.resolve((ClassSelector) selector, context);
        }
        if (selector instanceof NestedClassSelector) {
            return resolver.resolve((NestedClassSelector) selector, context);
        }
        if (selector instanceof DirectorySelector) {
            return resolver.resolve((DirectorySelector) selector, context);
        }
        if (selector instanceof FileSelector) {
            return resolver.resolve((FileSelector) selector, context);
        }
        if (selector instanceof MethodSelector) {
            return resolver.resolve((MethodSelector) selector, context);
        }
        if (selector instanceof NestedMethodSelector) {
            return resolver.resolve((NestedMethodSelector) selector, context);
        }
        if (selector instanceof ModuleSelector) {
            return resolver.resolve((ModuleSelector) selector, context);
        }
        if (selector instanceof PackageSelector) {
            return resolver.resolve((PackageSelector) selector, context);
        }
        if (selector instanceof UriSelector) {
            return resolver.resolve((UriSelector) selector, context);
        }
        return resolver.resolve(selector, context);
    });
}
private Optional<Resolution> resolve(DiscoverySelector selector,
        Function<SelectorResolver, Resolution> resolutionFunction) {
    // 12. resolutionFunction是第11步的lambda表达式
    return resolvers.stream()
            .map(resolutionFunction)
            .filter(Resolution::isResolved)
            .findFirst() // 触发lambda表达式执行
            .map(resolution -> {
                contextBySelector.remove(selector);
                resolvedSelectors.put(selector, resolution);
                resolution.getMatches()
                        .forEach(match -> resolvedUniqueIds.put(match.getTestDescriptor().getUniqueId(), match));
                return resolution;
            });
}

// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution
private Optional<Resolution> resolve(DiscoverySelector selector) {
    if (resolvedSelectors.containsKey(selector)) {
        return Optional.of(resolvedSelectors.get(selector));
    }
    if (selector instanceof UniqueIdSelector) {
        return resolveUniqueId((UniqueIdSelector) selector);
    }
    // 11. 执行resolver,重点关注ClassContainerSelectorResolver
    return resolve(selector, resolver -> {
        Context context = getContext(selector);
        if (selector instanceof ClasspathResourceSelector) {
            return resolver.resolve((ClasspathResourceSelector) selector, context);
        }
        if (selector instanceof ClasspathRootSelector) {
            // 13. 执行resolver的resolve()方法
            return resolver.resolve((ClasspathRootSelector) selector, context);
        }
        if (selector instanceof ClassSelector) {
            return resolver.resolve((ClassSelector) selector, context);
        }
        if (selector instanceof NestedClassSelector) {
            return resolver.resolve((NestedClassSelector) selector, context);
        }
        if (selector instanceof DirectorySelector) {
            return resolver.resolve((DirectorySelector) selector, context);
        }
        if (selector instanceof FileSelector) {
            return resolver.resolve((FileSelector) selector, context);
        }
        if (selector instanceof MethodSelector) {
            return resolver.resolve((MethodSelector) selector, context);
        }
        if (selector instanceof NestedMethodSelector) {
            return resolver.resolve((NestedMethodSelector) selector, context);
        }
        if (selector instanceof ModuleSelector) {
            return resolver.resolve((ModuleSelector) selector, context);
        }
        if (selector instanceof PackageSelector) {
            return resolver.resolve((PackageSelector) selector, context);
        }
        if (selector instanceof UriSelector) {
            return resolver.resolve((UriSelector) selector, context);
        }
        return resolver.resolve(selector, context);
    });
}

// 源码位置:org.junit.platform.engine.support.discovery.ClassContainerSelectorResolver
public Resolution resolve(ClasspathRootSelector selector, Context context) {
    return classSelectors(findAllClassesInClasspathRoot(selector.getClasspathRoot(), classFilter, classNameFilter));
}
private SelectorResolver.Resolution classSelectors(List<Class<?>> classes) {
    return classes.isEmpty() ? Resolution.unresolved() : Resolution.selectors((Set)classes.stream().map(DiscoverySelectors::selectClass).collect(Collectors.toSet()));
}
public static List<Class<?>> findAllClassesInClasspathRoot(URI root, Predicate<Class<?>> classFilter, Predicate<String> classNameFilter) {
    // 14. classFilter即前面提供的IsTestClassWithTests,查找指定目录(root)的类文件,并通过classFilter过滤
    //     ReflectionUtils里使用classpathScanner扫描测试用例文件,调用classFilter的test()方法进行过滤
    return ReflectionUtils.findAllClassesInClasspathRoot(root, classFilter, classNameFilter);
}
// 源码位置:org.junit.platform.commons.util.ReflectionUtils
public static List<Class<?>> findAllClassesInClasspathRoot(URI root, Predicate<Class<?>> classFilter, Predicate<String> classNameFilter) {
    return findAllClassesInClasspathRoot(root, ClassFilter.of(classNameFilter, classFilter));
}
public static List<Class<?>> findAllClassesInClasspathRoot(URI root, ClassFilter classFilter) {
    return Collections.unmodifiableList(classpathScanner.scanForClassesInClasspathRoot(root, classFilter));
}


// 源码位置:org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests
public class IsTestClassWithTests implements Predicate<Class<?>> {
    // 根据方法上有@Test注解过滤
    private static final IsTestMethod isTestMethod = new IsTestMethod();
    // 根据有@TestFactory注解过滤
    private static final IsTestFactoryMethod isTestFactoryMethod = new IsTestFactoryMethod();
    // 根据有@TestTemplate注解过滤
    private static final IsTestTemplateMethod isTestTemplateMethod = new IsTestTemplateMethod();
    
    // 15. 执行过滤
    public boolean test(Class<?> candidate) {
        return isPotentialTestContainer.test(candidate)
                && (hasTestOrTestFactoryOrTestTemplateMethods(candidate) || hasNestedTests(candidate));
    }
    // 省略其它代码
}

// 源码位置:org.junit.jupiter.engine.discovery.predicates.IsTestMethod
public class IsTestMethod extends IsTestableMethod {
    public IsTestMethod() {
        super(Test.class, true); // @Test注解
    }
}
// 源码位置:org.junit.jupiter.engine.discovery.predicates.IsTestFactoryMethod
public class IsTestFactoryMethod extends IsTestableMethod {
    public IsTestFactoryMethod() {
        super(TestFactory.class, false); //@TestFactory注解
    }
}
// 源码位置:org.junit.jupiter.engine.discovery.predicates.IsTestTemplateMethod
public class IsTestTemplateMethod extends IsTestableMethod {
    public IsTestTemplateMethod() {
        super(TestTemplate.class, true); // @TestTemplate注解
    }
}

// org.junit.jupiter.engine.discovery.predicates.IsTestableMethod
abstract class IsTestableMethod implements Predicate<Method> {
    private final Class<? extends Annotation> annotationType;
    private final boolean mustReturnVoid;

    IsTestableMethod(Class<? extends Annotation> annotationType, boolean mustReturnVoid) {
        this.annotationType = annotationType;
        this.mustReturnVoid = mustReturnVoid;
    }

    @Override
    public boolean test(Method candidate) {
        // Please do not collapse the following into a single statement.
        if (isStatic(candidate)) { // 不支持静态方法的测试用例
            return false;
        }
        if (isPrivate(candidate)) { // 不支持私有方法的测试用例
            return false;
        }
        if (isAbstract(candidate)) { // 不支持抽象方法的测试用例
            return false;
        }
        if (returnsVoid(candidate) != this.mustReturnVoid) { // 不支持返回值不是Void的测试用例
            return false;
        }

        // 16. 根据注解过滤
        return isAnnotated(candidate, this.annotationType);
    }
}

// org.junit.platform.commons.util.AnnotationUtils
public static boolean isAnnotated(AnnotatedElement element, Class<? extends Annotation> annotationType) {
    // 17. 查找对应的注解,如果注解存在则是满足要求的测试用例类
    //     即带有@Test、@TestFactory、@TestTemplate这三种注解的测试用例类才是符合要求的测试用例类
    return findAnnotation(element, annotationType).isPresent();
}

// 回到EngineDiscoveryRequestResolution,存储找到的测试用例
// 源码位置:org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution
private void resolveCompletely(DiscoverySelector selector) {
    EngineDiscoveryListener discoveryListener = request.getDiscoveryListener();
    UniqueId engineId = engineDescriptor.getUniqueId();
    try {
        // 10. 执行,参数为ClasspathRootSelector
        Optional<Resolution> result = resolve(selector);
        if (result.isPresent()) {
            discoveryListener.selectorProcessed(engineId, selector, resolved());
            // 17. 把找到的测试用例类存到变量中,供后面使用
            enqueueAdditionalSelectors(result.get());
        }
        else {
            discoveryListener.selectorProcessed(engineId, selector, unresolved());
        }
    }
    catch (Throwable t) {
        UnrecoverableExceptions.rethrowIfUnrecoverable(t);
        discoveryListener.selectorProcessed(engineId, selector, failed(t));
    }
}

2.2 规则

从上面原理看,可以分为文件和方法两个角度来分析。

从文件角度来说,得有@Test、@TestFactory、@TestTemplate三种注解中的一种,测试用例文件才会被加载到。@TestFactory是用于动态创建多个测试用例,感觉测试用例还是一个个分开写比较容易维护,此注解争取不使用。

@TestTemplate注解则可以帮助提供动态参数给测试用例,在某些场景可能需要用,建议封装到框架里使用。

从方法角度来看,私有方法、静态方法、抽象方法、有返回值的方法,都不能成为测试用例,需要规避这些情况。

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void should_say_string() throws Exception {
        String messge = "abc";
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/sayHello")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content("message=" + messge))
                .andExpect(status().isOk())
                .andReturn();
        assertThat(result.getResponse().getContentAsString()).isEqualTo("Hello world: " + messge);
    }

    @Test
    private String check_empty_in_private_method() {
        String str = "abc";
        assertThat(str).isNotEmpty();
        return str;
    }

    @Test
    public String check_empty_with_return_value() {
        String str = "abc";
        assertThat(str).isNotEmpty();
        return str;
    }

    @Test
    public static void check_empty_in_static_method() {
        String str = "abc";
        assertThat(str).isNotEmpty();
    }
}

3 架构一小步

规范:测试用例文件名用xxxTest.java为后缀。不使用Tests.java后缀,避免容易漏。

规范:测试用例方法都加上@Test注解,需要动态参数的地方用@TestTemplate,尽量不用@TestFactory。

规范:测试用例方法名使用下划线的方式,用表意的方式描述清楚测试用例的用途,长度可以长一些。

规范:测试用例方法名不需要指定test关键字。

规范:测试用例方法必须为public、非静态、非抽象、无返回值方法。

相关推荐
测试老哥2 天前
Python+Selenium实现自动化测试
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
测试老哥2 天前
软件测试之单元测试
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
测试19982 天前
软件测试之压力测试总结
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·压力测试
程序员三藏4 天前
如何使用Pytest进行测试?
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
Aric_Jones5 天前
业务测试用例
测试用例
一个处女座的测试9 天前
接口自动化测试实战:测试用例也能自动生成
测试用例
草履虫建模11 天前
Postman - API 调试与开发工具 - 标准使用流程
java·测试工具·spring·json·测试用例·postman·集成学习
小叶爱吃鱼11 天前
软件测试-测试用例,举例说明
功能测试·测试用例