Junit源码阅读(四)之自定义扩展


前言

上次的博客中我们着重介绍了Junit的Validator机制,这次我们将聚焦到自定义扩展Rule上来。在很多情形下我们需要在测试过程中加入一些自定义的动作,这些就需要对statement进行包装,Junit为此提供了以TestRule接口和RunRules为基础的Rule扩展机制。

基本模型

首选TestRule由注解@ClassRule来指定,下面我们先给出TestRule的定义。

public interface TestRule {
    /**
     * Modifies the method-running {@link Statement} to implement this
     * test-running rule.
     *
     * @param base The {@link Statement} to be modified
     * @param description A {@link Description} of the test implemented in {@code base}
     * @return a new statement, which may be the same as {@code base},
     *         a wrapper around {@code base}, or a completely new Statement.
     */
    Statement apply(Statement base, Description description);
}

代码中的注释十分清楚,TestRule提供结合Description为原Statement附加功能转变为新的Statement的apply方法。RunRules则是一系列TestRule作用后得到的Statement,如下:

public class RunRules extends Statement {
    private final Statement statement;

    public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
        statement = applyAll(base, rules, description);
    }

    @Override
    public void evaluate() throws Throwable {
        statement.evaluate();
    }

    private static Statement applyAll(Statement result, Iterable<TestRule> rules,
            Description description) {
        for (TestRule each : rules) {
            result = each.apply(result, description);
        }
        return result;
    }
}

原理解释

那么RunRules又是如何在我们的测试运行过程中被转化的呢?还记得在第二篇博客中我们提到了在classBlock方法中statement会被withBeforeClasses等装饰,同样此处它也被withClassRules装饰。首先由testClass返回带@ClassRule注解的对应值,分别由getAnnotatedFieldValues和getAnnotatedMethodValues方法提供。之后我们将这些值转化为TestRule对象,然后将这个TestRule列表和原有的statement结合返回RunRules。

    private Statement withClassRules(Statement statement) {
        List<TestRule> classRules = classRules();
        return classRules.isEmpty() ? statement :
                new RunRules(statement, classRules, getDescription());
    }

TimeOut示例

接下来我们以超时扩展为示例来看一看一个扩展是如何起作用的。

public class Timeout implements TestRule {
    private final long timeout;
    private final TimeUnit timeUnit;
    private final boolean lookForStuckThread;

    
    public static Builder builder() {
        return new Builder();
    }

    
    @Deprecated
    public Timeout(int millis) {
        this(millis, TimeUnit.MILLISECONDS);
    }

    
    public Timeout(long timeout, TimeUnit timeUnit) {
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        lookForStuckThread = false;
    }

    
    protected Timeout(Builder builder) {
        timeout = builder.getTimeout();
        timeUnit = builder.getTimeUnit();
        lookForStuckThread = builder.getLookingForStuckThread();
    }

    
    public static Timeout millis(long millis) {
        return new Timeout(millis, TimeUnit.MILLISECONDS);
    }

    
    public static Timeout seconds(long seconds) {
        return new Timeout(seconds, TimeUnit.SECONDS);
    }

    
    protected final long getTimeout(TimeUnit unit) {
        return unit.convert(timeout, timeUnit);
    }

    
    protected final boolean getLookingForStuckThread() {
        return lookForStuckThread;
    }

    
    protected Statement createFailOnTimeoutStatement(
            Statement statement) throws Exception {
        return FailOnTimeout.builder()
            .withTimeout(timeout, timeUnit)
            .withLookingForStuckThread(lookForStuckThread)
            .build(statement);
    }

    public Statement apply(Statement base, Description description) {
        try {
            return createFailOnTimeoutStatement(base);
        } catch (final Exception e) {
            return new Statement() {
                @Override public void evaluate() throws Throwable {
                    throw new RuntimeException("Invalid parameters for Timeout", e);
                }
            };
        }
    }

    
    public static class Builder {
        private boolean lookForStuckThread = false;
        private long timeout = 0;
        private TimeUnit timeUnit = TimeUnit.SECONDS;

        protected Builder() {
        }

        
        public Builder withTimeout(long timeout, TimeUnit unit) {
            this.timeout = timeout;
            this.timeUnit = unit;
            return this;
        }

        protected long getTimeout() {
            return timeout;
        }

        protected TimeUnit getTimeUnit()  {
            return timeUnit;
        }

        
        public Builder withLookingForStuckThread(boolean enable) {
            this.lookForStuckThread = enable;
            return this;
        }

        protected boolean getLookingForStuckThread() {
            return lookForStuckThread;
        }


        /**
         * Builds a {@link Timeout} instance using the values in this builder.,
         */
        public Timeout build() {
            return new Timeout(this);
        }
    }
}

我们可以看到上述最核心的就是createFailOnTimeoutStatement方法,它直接返回了一个FailOnTimeout,并且用它内建的Builder初始化。下面我们仅仅给出FailOnTimeout内部的域以及一些核心方法。

public class FailOnTimeout extends Statement {
    private final Statement originalStatement;
    private final TimeUnit timeUnit;
    private final long timeout;
    private final boolean lookForStuckThread;

    private FailOnTimeout(Builder builder, Statement statement) {
        originalStatement = statement;
        timeout = builder.timeout;
        timeUnit = builder.unit;
        lookForStuckThread = builder.lookForStuckThread;
    }


    public static class Builder {
        private boolean lookForStuckThread = false;
        private long timeout = 0;
        private TimeUnit unit = TimeUnit.SECONDS;

        private Builder() {
        }

        public FailOnTimeout build(Statement statement) {
            if (statement == null) {
                throw new NullPointerException("statement cannot be null");
            }
            return new FailOnTimeout(this, statement);
        }
    }


    @Override
    public void evaluate() throws Throwable {
        CallableStatement callable = new CallableStatement();
        FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
        ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
        Thread thread = new Thread(threadGroup, task, "Time-limited test");
        thread.setDaemon(true);
        thread.start();
        callable.awaitStarted();
        Throwable throwable = getResult(task, thread);
        if (throwable != null) {
            throw throwable;
        }
    }

    private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
        try {
            if (timeout > 0) {
                return task.get(timeout, timeUnit);
            } else {
                return task.get();
            }
        } catch (InterruptedException e) {
            return e; // caller will re-throw; no need to call Thread.interrupt()
        } catch (ExecutionException e) {
            // test failed; have caller re-throw the exception thrown by the test
            return e.getCause();
        } catch (TimeoutException e) {
            return createTimeoutException(thread);
        }
    }

    private class CallableStatement implements Callable<Throwable> {
        private final CountDownLatch startLatch = new CountDownLatch(1);

        public Throwable call() throws Exception {
            try {
                startLatch.countDown();
                originalStatement.evaluate();
            } catch (Exception e) {
                throw e;
            } catch (Throwable e) {
                return e;
            }
            return null;
        }

        public void awaitStarted() throws InterruptedException {
            startLatch.await();
        }
    }

}

可以看出它通过内置的Builder类来配置参数,通过CallableStatement和FutureTask启动新线程来运行真实的测试样例,并使用CountDownLatch来让父进程等待。实际的超时判断则借助了FutureTask的getResult,如果规定时间未返回结果就抛出超时异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Spring Boot 中,通常按照 Maven 或 Gradle 的标准目录结构来组织应用程序的代码和资源。这些标准目录结构如下: - `src/main/java`: Java 代码文件,包括应用程序的主入口文件和其他类文件。 - `src/main/resources`: 应用程序的资源文件,包括配置文件、静态资源文件等。 - `src/test/java`: 测试代码文件,包括单元测试和集成测试等。 - `src/test/resources`: 测试使用的资源文件。 如果需要自定义目录标准,可以使用 Spring Boot 提供的 `@SpringBootApplication` 注解的 `@ComponentScan`、`@EntityScan` 和 `@EnableJpaRepositories` 属性来指定自定义的目录。 例如,可以通过 `@ComponentScan` 注解来指定自定义的 Java 代码路径: ```java @SpringBootApplication @ComponentScan(basePackages = {"com.example.custom"}) public class MyApplication { //... } ``` 还可以使用 `@EntityScan` 和 `@EnableJpaRepositories` 注解来指定自定义的实体类和 JPA 存储库路径: ```java @SpringBootApplication @EntityScan(basePackages = {"com.example.custom.entity"}) @EnableJpaRepositories(basePackages = {"com.example.custom.repository"}) public class MyApplication { //... } ``` 需要注意的是,如果自定义了目录标准,也需要相应地调整 Maven 或 Gradle 的配置文件来指定对应的目录路径。 ### 回答2: Spring Boot是一个开源的Java框架,用于构建独立的、可执行的、基于微服务的应用程序。它提供了许多默认的配置和规范,但也允许开发者根据自己的需求进行自定义。 在Spring Boot中,可以自定义目录标准以满足项目的特定需求。下面是一些常用的自定义目录: 1. 数据持久化目录:可以将项目的数据库文件或数据文件存放在自定义的目录下。可以使用`spring.datasource.url`和`spring.datasource.username`等属性来指定数据库文件的路径和用户名。 2. 静态资源目录:默认情况下,Spring Boot会将静态资源(如CSS、JS和图片文件)放在`/resources/static`目录下。如果需要,可以在`application.properties`文件中使用`spring.resources.static-locations`属性来自定义静态资源的目录。 3. 模板文件目录:Spring Boot支持使用模板引擎来渲染视图。默认情况下,模板文件应放在`/resources/templates`目录下。如果需要,可以在`application.properties`中使用`spring.thymeleaf.prefix`(对于Thymeleaf模板引擎)或`spring.freemarker.template-loader-path`(对于FreeMarker模板引擎)属性来自定义模板文件的目录。 4. 日志文件目录:Spring Boot使用Logback作为默认的日志框架,并将日志文件存放在`/logs`目录下。如果需要,可以在`application.properties`中使用`logging.file.path`属性来自定义日志文件的目录和文件名。 5. 测试文件目录:测试文件(如JUnit测试类、Mock数据文件等)默认会放在`/src/test`目录下。如果需要,可以使用Maven的`src/test/resources`目录来存放测试资源文件。 总的来说,Spring Boot的目录结构和标准已经能够满足大部分项目的需求。如果需要进行自定义,可以通过调整相关的属性来修改默认的目录设置。需要注意的是,在进行自定义目录时,应确保修改配置文件中的相关属性,并遵循项目的规范和最佳实践。 ### 回答3: Spring Boot提供了一套自定义目录标准,以便开发人员能根据自己的需求对项目的结构进行灵活调整。 1. 源码目录结构: - `src/main/java`:Java源文件所在目录,主要用于存放应用程序的源代码。 - `src/main/resources`:资源文件所在目录,主要用于存放配置文件、静态资源和模板文件等。 - `src/main/webapp`:Web应用的根目录,用于存放Web容器需要访问的文件。 2. 测试目录结构: - `src/test/java`:单元测试源文件所在目录,主要用于存放单元测试的源代码。 - `src/test/resources`:测试资源文件所在目录,用于存放测试过程中使用的配置文件、静态资源和模板文件等。 3. 项目构建目录结构: - `src/main/resources/static`:静态资源文件所在目录,用于存放各种静态资源文件,如css、js、图片等。 - `src/main/resources/templates`:模板文件所在目录,用于存放各种模板文件,如Thymeleaf、Freemarker等。 - `src/main/resources/application.properties`:应用程序的配置文件,可以设置各种属性和参数。 - `src/main/resources/application.yml`:应用程序的配置文件,使用YAML格式,可以与properties文件互相转换。 - `src/main/resources/banner.txt`:应用程序启动时的自定义启动图标文件。 4. 其他目录结构: - `src/main/resources/META-INF`:存放项目的元信息文件,如MANIFEST.MF等。 - `src/main/resources/logback.xml`:日志配置文件,用于配置日志输出的格式和方式。 - `src/main/resources/spring/`:存放Spring相关的配置文件,如Spring MVC的配置文件。 - `src/main/resources/db/`:存放数据库相关的配置文件,如MyBatis的配置文件。 通过遵循这些自定义目录标准,可以使项目结构更加清晰、易读,并提高开发效率和可维护性。当然,这些目录标准并非强制要求,开发人员可以根据自己的项目需求进行适当的调整和扩展
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值