SpringBoot单元测试

406 阅读7分钟

单元测试的主要框架是 JUnit , 在JUnit 运行一个单元测试时,会寻找 @RunWith注解指定的 Runner类,如果没有指定@RunWith则默认会以 JUnit4来运行 以下是 @RunWith的定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface RunWith {
    /**
    * @return a Runner class (must have a constructor that takes a single Class to run)
    */
    Class<? extends Runner> value();
}
在Junit4 框架里,每一个单元测试类都会创建一个 `Runner`进行对应,默认是 `Junit4`
public abstract class Runner implements Describable {
    /*
     * (non-Javadoc)
     * @see org.junit.runner.Describable#getDescription()
     */
    public abstract Description getDescription();

    /**
     * 调用Runner的run方法进行单元测试的执行
     * Run the tests for this runner.
     *
     * @param notifier will be notified of events while tests are being run--tests being
     * started, finishing, and failing
     */
    public abstract void run(RunNotifier notifier);

    /**
     * @return the number of tests to be run by the receiver
     */
    public int testCount() {
        return getDescription().testCount();
    }
}
// 主要找到run的方法实现
public final class JUnit4 extends BlockJUnit4ClassRunner {
    /**
     * Constructs a new instance of the default runner
     */
    public JUnit4(Class<?> klass) throws InitializationError {
        super(klass);
    }
}

从代码继承关系里可以找到,是在 ParentRunner里实现

public abstract class ParentRunner<T> extends BlockJUnit4ClassRunner {
        @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            // Statement 代表了运行时的一个或多个动作,如 BeforeClass,AfterClass,Before,After等
            Statement statement = classBlock(notifier);
            // 依次调用对应的方法执行
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }
    // 构建Class级别的动作,如BeforeClass和AfterClass
    protected Statement classBlock(final RunNotifier notifier) {
        // 因为statement是一个链表,先构建一个包含方法级的statement
        // 所有标注了 @Test的方法
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            // 在外层加一个类级别的BeforeClass,需要是 static方法
            statement = withBeforeClasses(statement);
            // 在最后加一个AfterClass
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
    
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
    
    private void runChildren(final RunNotifier notifier) {
        final RunnerScheduler currentScheduler = scheduler;
        try {
            // 获取所有有标识@Test注解的方法
            for (final T each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        // 执行每个单元测试,在后面的 BlockJUnit4ClassRunner分析
                        ParentRunner.this.runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }
 
    private Collection<T> getFilteredChildren() {
        if (filteredChildren == null) {
            synchronized (childrenLock) {
                if (filteredChildren == null) {
                    // 暂存所有的方法,由子类BlockJUnit4ClassRunner实现 
                    filteredChildren = Collections.unmodifiableCollection(getChildren());
                }
            }
        }
        return filteredChildren;
    }
}
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
    @Override
    protected List<FrameworkMethod> getChildren() {
        return computeTestMethods();
    }
    // 获取所有@Test注解的方法
    protected List<FrameworkMethod> computeTestMethods() {
        return getTestClass().getAnnotatedMethods(Test.class);
    } 
    
    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        // 如果方法标注了 @Ignore,则不会执行
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
        } else {
            // 执行每个方法
            // 这里会
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
    protected Statement methodBlock(FrameworkMethod method) {
        Object test;
        // 1. 创建当前类的实例
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }
        // 创建方法的Statement
        Statement statement = methodInvoker(method, test);
        statement = possiblyExpectingExceptions(method, test, statement);
        //封装一个设置了超时线程,以@Test上设置的timeout值
        statement = withPotentialTimeout(method, test, statement);
        // 封装一层方法级 @Before的方法
        statement = withBefores(method, test, statement);
        // 封装一层方法级 @After的方法
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        return statement;
    }
}

以上是 JUnit4框架的对应各个时机点的流程方式 Spring框架的单元测试同样也是基于 BlockJUnit4ClassRunner,但因为要处理 Spring的Context,需要进行初始化,是如何处理的呢? 入口为 SpringRunner

public final class SpringRunner extends SpringJUnit4ClassRunner {

	/**
	 * Construct a new {@code SpringRunner} and initialize a
	 * {@link org.springframework.test.context.TestContextManager TestContextManager}
	 * to provide Spring testing functionality to standard JUnit 4 tests.
	 * @param clazz the test class to be run
	 * @see #createTestContextManager(Class)
	 */
	public SpringRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
	}

}
	public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
		if (logger.isDebugEnabled()) {
			logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
		}
		ensureSpringRulesAreNotPresent(clazz);
        // 初始化Spring的TestContextManager
		this.testContextManager = createTestContextManager(clazz);
	}

	/**
	 * Create a new {@link TestContextManager} for the supplied test class.
	 * <p>Can be overridden by subclasses.
	 * @param clazz the test class to be managed
	 */
	protected TestContextManager createTestContextManager(Class<?> clazz) {
		return new TestContextManager(clazz);
	}

    public TestContextManager(Class<?> testClass) {
        // 1. 创建ClassLoader DefaultCacheAwareContextLoaderDelegate,该类调用的是无参构造函数,默认会初始会一个静态的DefaultContextCache
        // 进行缓存下spring ApplicationContext,这里是 Spring Test framework ApplicationContext 缓存的逻辑,即相同的配置下,ApplicationContext是相同的
        // 2. 创建 DefaultBootstrapContext  -> 加载springContext
        // 3. 创建 TestContextBootstrapper -> WebTestContextBootstrapper
		this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));
	}

	/**
	 * Construct a new {@code TestContextManager} using the supplied {@link TestContextBootstrapper}
	 * and {@linkplain #registerTestExecutionListeners register} the necessary
	 * {@link TestExecutionListener TestExecutionListeners}.
	 * <p>Delegates to the supplied {@code TestContextBootstrapper} for building
	 * the {@code TestContext} and retrieving the {@code TestExecutionListeners}.
	 * @param testContextBootstrapper the bootstrapper to use
	 * @see TestContextBootstrapper#buildTestContext
	 * @see TestContextBootstrapper#getTestExecutionListeners
	 * @see #registerTestExecutionListeners
	 */
	public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
        // 这里默认的是 DefaultTestContext
		this.testContext = testContextBootstrapper.buildTestContext();
        // 注册所有的 TestExecutionListener
        // 如果类上有标注 @TestExecutionListeners ,则为类上指定的为主
        // 如果没有标注,则会获取 spring.factories 文件定义的所有类进行排序
        // 而这Listerner 是Spring的context 初始化的时机点
		registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
	}

	/**
	 * Get the {@link TestContext} managed by this {@code TestContextManager}.
	 */
	public final TestContext getTestContext() {
		return this.testContextHolder.get();
	}

	/**
	 * Register the supplied list of {@link TestExecutionListener TestExecutionListeners}
	 * by appending them to the list of listeners used by this {@code TestContextManager}.
	 * @see #registerTestExecutionListeners(TestExecutionListener...)
	 */
	public void registerTestExecutionListeners(List<TestExecutionListener> testExecutionListeners) {
		registerTestExecutionListeners(testExecutionListeners.toArray(new TestExecutionListener[0]));
	}

	/**
	 * Register the supplied array of {@link TestExecutionListener TestExecutionListeners}
	 * by appending them to the list of listeners used by this {@code TestContextManager}.
	 */
	public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) {
		for (TestExecutionListener listener : testExecutionListeners) {
			if (logger.isTraceEnabled()) {
				logger.trace("Registering TestExecutionListener: " + listener);
			}
			this.testExecutionListeners.add(listener);
		}
	}

上面提到 Spring Context是在 TestExecutionListener中进行初始化,具体是怎么做的呢?

org.springframework.test.context.TestExecutionListener = \
	org.springframework.test.context.web.ServletTestExecutionListener,\
	org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
	org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
	org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
	org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
	org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener

以上的几个是针对不同场景的处理方式,分析下 DependencyInjectionTestExecutionListener

public class DependencyInjectionTestExecutionListener extends AbstractTestExecutionListener {
    
    /**
    * Attribute name for a {@link TestContext} attribute which indicates
    * whether or not the dependencies of a test instance should be
    * <em>reinjected</em> in
    * {@link #beforeTestMethod(TestContext) beforeTestMethod()}. Note that
    * dependencies will be injected in
    * {@link #prepareTestInstance(TestContext) prepareTestInstance()} in any
    * case.
    * <p>Clients of a {@link TestContext} (e.g., other
    * {@link org.springframework.test.context.TestExecutionListener TestExecutionListeners})
    * may therefore choose to set this attribute to signal that dependencies
    * should be reinjected <em>between</em> execution of individual test
    * methods.
    * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
    */
    public static final String REINJECT_DEPENDENCIES_ATTRIBUTE = Conventions.getQualifiedAttributeName(
        DependencyInjectionTestExecutionListener.class, "reinjectDependencies");
    
    private static final Log logger = LogFactory.getLog(DependencyInjectionTestExecutionListener.class);
    
    
    /**
    * Returns {@code 2000}.
    */
    @Override
    public final int getOrder() {
        return 2000;
    }
    
    /**
    * Performs dependency injection on the
    * {@link TestContext#getTestInstance() test instance} of the supplied
    * {@link TestContext test context} by
    * {@link AutowireCapableBeanFactory#autowireBeanProperties(Object, int, boolean) autowiring}
    * and
    * {@link AutowireCapableBeanFactory#initializeBean(Object, String) initializing}
    * the test instance via its own
    * {@link TestContext#getApplicationContext() application context} (without
    * checking dependencies).
    * <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed
    * from the test context, regardless of its value.
    */
    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("Performing dependency injection for test context [" + testContext + "].");
        }
        injectDependencies(testContext);
    }
    
    /**
    * If the {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} in the supplied
    * {@link TestContext test context} has a value of {@link Boolean#TRUE},
    * this method will have the same effect as
    * {@link #prepareTestInstance(TestContext) prepareTestInstance()};
    * otherwise, this method will have no effect.
    */
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        if (Boolean.TRUE.equals(testContext.getAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE))) {
            if (logger.isDebugEnabled()) {
                logger.debug("Reinjecting dependencies for test context [" + testContext + "].");
            }
            injectDependencies(testContext);
        }
    }
    
    // 当前的类进行注册到Spring Bean中,并进行初始化
    protected void injectDependencies(TestContext testContext) throws Exception {
        Object bean = testContext.getTestInstance();
        // 会进行Spring的容器初始化
        AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
        beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
        beanFactory.initializeBean(bean, testContext.getTestClass().getName());
        testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
    }
    
}

那这些 TestExecutionListener 是在哪里被调用呢? 在SpringJUnit4ClassRunner中有重写 BlockJUnit4ClassRunner的内容

    //创建好对象后
    @Override
	protected Object createTest() throws Exception {
		Object testInstance = super.createTest();
        // 调用 TestExecutionListener.prepareTestInstance
		getTestContextManager().prepareTestInstance(testInstance);
		return testInstance;
	}

	/**
	 * Perform the same logic as
	 * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},
	 * except that tests are determined to be <em>ignored</em> by
	 * {@link #isTestMethodIgnored(FrameworkMethod)}.
	 */
	@Override
	protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) {
		Description description = describeChild(frameworkMethod);
		if (isTestMethodIgnored(frameworkMethod)) {
			notifier.fireTestIgnored(description);
		}
		else {
			Statement statement;
			try {
				statement = methodBlock(frameworkMethod);
			}
			catch (Throwable ex) {
				statement = new Fail(ex);
			}
			runLeaf(statement, description, notifier);
		}
	}

	// 重写了基类的 methodBlock
    // 1. 创建对象,调用 prepareTestInstance方法
    // 2. 调用
	@Override
	protected Statement methodBlock(FrameworkMethod frameworkMethod) {
		Object testInstance;
		try {
			testInstance = new ReflectiveCallable() {
				@Override
				protected Object runReflectiveCall() throws Throwable {
					return createTest();
				}
			}.run();
		}
		catch (Throwable ex) {
			return new Fail(ex);
		}

		Statement statement = methodInvoker(frameworkMethod, testInstance);
        // 调用 listener的 beforeTestExecution
		statement = withBeforeTestExecutionCallbacks(frameworkMethod, testInstance, statement);
		//  listener的 beforeTestExecution注册
        statement = withAfterTestExecutionCallbacks(frameworkMethod, testInstance, statement);
		// 注册异常的处理方式 
        statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
		// 方法 beforeTestMethod, @Before的处理方式 
        statement = withBefores(frameworkMethod, testInstance, statement);
        // 方法 afterTestMethod, @After的处理方式 
		statement = withAfters(frameworkMethod, testInstance, statement);
		statement = withRulesReflectively(frameworkMethod, testInstance, statement);
		statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
		statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
		return statement;
	}

    // 注册 listener的 beforeTestClass
	@Override
	protected Statement withBeforeClasses(Statement statement) {
		Statement junitBeforeClasses = super.withBeforeClasses(statement);
		return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());
	}

    // 注册 listener的 afterTestClass
	@Override
	protected Statement withAfterClasses(Statement statement) {
		Statement junitAfterClasses = super.withAfterClasses(statement);
		return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());
	}

在JUnit4的基础上,调用对应的Listener,同时进行Spring context的初始化包括Bean的创建 注意下DefaultTestContext ,这个类是和testClass 具体的类关联的,在加载Spring ApplicationContext的逻辑如下

	public ApplicationContext getApplicationContext() {
        // 从cacheAwareContextLoaderDelegate中加载数据
        // 从上面知道是 DefaultCacheAwareContextLoaderDelegate
		ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
		if (context instanceof ConfigurableApplicationContext) {
			@SuppressWarnings("resource")
			ConfigurableApplicationContext cac = (ConfigurableApplicationContext) context;
			Assert.state(cac.isActive(), () ->
					"The ApplicationContext loaded for [" + mergedContextConfiguration +
					"] is not active. This may be due to one of the following reasons: " +
					"1) the context was closed programmatically by user code; " +
					"2) the context was closed during parallel test execution either " +
					"according to @DirtiesContext semantics or due to automatic eviction " +
					"from the ContextCache due to a maximum cache size policy.");
		}
		return context;
	}

如下:

	@Override
	public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
		synchronized (this.contextCache) {
            // this.contextCache 默认是一个静态的数据
            // 从这里可以看到,缓存的key是MergedContextConfiguration
			ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
			if (context == null) {
				try {
					context = loadContextInternal(mergedContextConfiguration);
					if (logger.isDebugEnabled()) {
						logger.debug(String.format("Storing ApplicationContext in cache under key [%s]",
								mergedContextConfiguration));
					}
					this.contextCache.put(mergedContextConfiguration, context);
				}
				catch (Exception ex) {
					throw new IllegalStateException("Failed to load ApplicationContext", ex);
				}
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s]",
							mergedContextConfiguration));
				}
			}

			this.contextCache.logStatistics();

			return context;
		}
	}

从上面分析可知,如果要使用同一个Spring ApplicationContext,需要 MergedContextConfiguration对应的数据也是相同的,如果在运行中,使用的 ApplicationContext 非同一个,则需要考虑MergedContextConfiguration是否相同