单元测试加上@Transactional就能实现回滚【原理】


前言

有时候我们开发需求的过程中写单测,会往数据库里插入数据,但是这样的数据其实是没有意义的,要么测试数据中含有唯一主键,每次run之前都要改单测里的参数,要么,有时候公司上线的流水线会跑一遍单测,给数据库多次插入无意义的数据。那么我们就需要让单测在run完之后可以自动回滚。

一、问题

如果保证单测插入到数据库的数据回滚?

二、回答

最简单的方式就是在方法上加上@Transactional注解了

三、源码分析

1.TestContextManager

{@code TestContextManager} is the main entry point into the <em>Spring
 * TestContext Framework</em>.
  TestContextManager是Spring测试框架的主要进入点
 *
 * <p>Specifically, a {@code TestContextManager} is responsible for managing a
 * single {@link TestContext} and signaling events to all registered
 * {@link TestExecutionListener TestExecutionListeners} at the following test
 <p>具体来说,{@code TestContextManager} 负责管理单个 {@link TestContext} 并在以下测试中向所有注册的 {@link TestExecutionListener TestExecutionListeners} 发送信号事件

根据上面的Java doc,我们便知道了两个重要的概念

TestContextTestExecutionListener

剩下的就是listener和testContext的交互了

2.TestContext

{@code TestContext} encapsulates the context in which a test is executed,
 * agnostic of the actual testing framework in use.
 {@code TestContext} 封装了执行测试的上下文,与使用的实际测试框架无关。

可以来看看默认实现DefaultTestContext

public class DefaultTestContext implements TestContext {

	private final Map<String, Object> attributes = new ConcurrentHashMap<>(4);

	private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;

	private final MergedContextConfiguration mergedContextConfiguration;

	private final Class<?> testClass;

	@Nullable
	private volatile Object testInstance;

	@Nullable
	private volatile Method testMethod;

	@Nullable
	private volatile Throwable testException;
}

对于我们一般的方法,最重要的其实就是testClass和testMethod

3.TestExecutionListener

3.1 AbstractTestContextBootstrapper#getTestExecutionListeners

testExecutionListeners是通过SpringFactory从spring.factories文件里的配置加载进来的
其中包含了我们本文关注的TransactionalTestExecutionListener

对于从spring.factories中加载配置类不熟悉的同学,可以参考这篇博客

3.2 钩子函数

其实钩子函数可以理解为静态AOP,也可以理解为spring框架开放给用户在某个时间点实现某些功能的扩展点。在spring源码里特别常见,譬如spring里的各种postProcessor的接口实现类,里面其实没有方法逻辑,用户可以通过自定义这些接口来在spring的bean的生命周期内对bean的实现进行个性化。
言归正传,咱们还是来看TestExecutionListener的钩子函数

/**
	 * Pre-processes a test class <em>before</em> execution of all tests within
	 * the class.
	 * <p>This method should be called immediately before framework-specific
	 * <em>before class</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 3.0
	 */
	default void beforeTestClass(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Prepares the {@link Object test instance} of the supplied
	 * {@link TestContext test context}, for example by injecting dependencies.
	 * <p>This method should be called immediately after instantiation of the test
	 * instance but prior to any framework-specific lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 */
	default void prepareTestInstance(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Pre-processes a test <em>before</em> execution of <em>before</em>
	 * lifecycle callbacks of the underlying test framework &mdash; for example,
	 * by setting up test fixtures.
	 * <p>This method <strong>must</strong> be called immediately prior to
	 * framework-specific <em>before</em> lifecycle callbacks. For historical
	 * reasons, this method is named {@code beforeTestMethod}. Since the
	 * introduction of {@link #beforeTestExecution}, a more suitable name for
	 * this method might be something like {@code beforeTestSetUp} or
	 * {@code beforeEach}; however, it is unfortunately impossible to rename
	 * this method due to backward compatibility concerns.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @see #afterTestMethod
	 * @see #beforeTestExecution
	 * @see #afterTestExecution
	 */
	default void beforeTestMethod(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Pre-processes a test <em>immediately before</em> execution of the
	 * {@link java.lang.reflect.Method test method} in the supplied
	 * {@link TestContext test context} &mdash; for example, for timing
	 * or logging purposes.
	 * <p>This method <strong>must</strong> be called after framework-specific
	 * <em>before</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 5.0
	 * @see #beforeTestMethod
	 * @see #afterTestMethod
	 * @see #afterTestExecution
	 */
	default void beforeTestExecution(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test <em>immediately after</em> execution of the
	 * {@link java.lang.reflect.Method test method} in the supplied
	 * {@link TestContext test context} &mdash; for example, for timing
	 * or logging purposes.
	 * <p>This method <strong>must</strong> be called before framework-specific
	 * <em>after</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 5.0
	 * @see #beforeTestMethod
	 * @see #afterTestMethod
	 * @see #beforeTestExecution
	 */
	default void afterTestExecution(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test <em>after</em> execution of <em>after</em>
	 * lifecycle callbacks of the underlying test framework &mdash; for example,
	 * by tearing down test fixtures.
	 * <p>This method <strong>must</strong> be called immediately after
	 * framework-specific <em>after</em> lifecycle callbacks. For historical
	 * reasons, this method is named {@code afterTestMethod}. Since the
	 * introduction of {@link #afterTestExecution}, a more suitable name for
	 * this method might be something like {@code afterTestTearDown} or
	 * {@code afterEach}; however, it is unfortunately impossible to rename
	 * this method due to backward compatibility concerns.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method was
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @see #beforeTestMethod
	 * @see #beforeTestExecution
	 * @see #afterTestExecution
	 */
	default void afterTestMethod(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test class <em>after</em> execution of all tests within
	 * the class.
	 * <p>This method should be called immediately after framework-specific
	 * <em>after class</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 3.0
	 */
	default void afterTestClass(TestContext testContext) throws Exception {
		/* no-op */
	}

由于本文今天关注的重点是如何实现回滚的,那么咱们需要care的其实就是TransactionalTestExecutionListener的beforeTestMethod和afterTestMethod。

3.3 TransactionalTestExecutionListener

3.3.1 beforeTestMethod

public void beforeTestMethod(final TestContext testContext) throws Exception {
		Method testMethod = testContext.getTestMethod();
		Class<?> testClass = testContext.getTestClass();
		Assert.notNull(testMethod, "Test method of supplied TestContext must not be null");

		TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
		Assert.state(txContext == null, "Cannot start new transaction without ending existing transaction");

		PlatformTransactionManager tm = null;
    // 获取方法或者类上的@Transactional注解信息
		TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testClass);
		// 如果加了@Transactional注解
		if (transactionAttribute != null) {
			transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(testContext,
				transactionAttribute);

			if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
				return;
			}
// transactionManager默认情况下会获取容器里的PlatformTransactionManager
			tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
			Assert.state(tm != null,
					() -> "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
		}
		if (tm != null) {
      // 需要重点关注isRollback方法,新建了一个事务上下文txContext
			txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
			runBeforeTransactionMethods(testContext);
      // 这里面通过TransactionManager的getTransaction获取了对应的transactionStatus,然后设置到了transactionContext上了
			txContext.startTransaction();
      // 将transactionContext放到了ThreadLocal上
			TransactionContextHolder.setCurrentTransactionContext(txContext);
		}
	}
3.3.3.1 isRollback
protected final boolean isRollback(TestContext testContext) throws Exception {
  // 如果类上面没有加@Rollback注解,那么此处rollback = true;只有@Rollback(value=false),此处才是false
		boolean rollback = isDefaultRollback(testContext);
  // 获取方法上的@Rollback注解
		Rollback rollbackAnnotation =
				AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class);
		if (rollbackAnnotation != null) {
      // 返回方法上的@Rollback注解的value
			boolean rollbackOverride = rollbackAnnotation.value();
			rollback = rollbackOverride;
		}
		else {
		}
  // 所以此处默认是true
		return rollback;
	}
3.3.1.2 isDefaultRollback
protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
		Class<?> testClass = testContext.getTestClass();
		Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class);
		boolean rollbackPresent = (rollback != null);
		if (rollbackPresent) {
			boolean defaultRollback = rollback.value();
			return defaultRollback;
		}
		// else
		return true;
	}
3.3.3.3 TransactionContext
TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager,
			TransactionDefinition transactionDefinition, boolean defaultRollback) {
		this.testContext = testContext;
		this.transactionManager = transactionManager;
		this.transactionDefinition = transactionDefinition;
		this.defaultRollback = defaultRollback;
  // 因此此处的flaggedForRollback被设置成了true
		this.flaggedForRollback = defaultRollback;
	}

综上,可以发现,在beforeTestMethod中,会读取方法的@Transactional注解,如果类或者方法上没有@Rollback,那么默认的会设置一个 TransactionContext,并将其flaggedForRollback设置为true

3.3.2 afterTestMethod

public void afterTestMethod(TestContext testContext) throws Exception {
		Method testMethod = testContext.getTestMethod();
		Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
		// 从ThreadLocal上拿到了事务上下文txContext
		TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
		// If there was (or perhaps still is) a transaction...
		if (txContext != null) {
			TransactionStatus transactionStatus = txContext.getTransactionStatus();
			try {
				// If the transaction is still active...
        // 获取事务状态transactionStatus,
				if (transactionStatus != null && !transactionStatus.isCompleted()) {
          // 在该方法里进行回滚
					txContext.endTransaction();
				}
			}
			finally {
				runAfterTransactionMethods(testContext);
			}
		}
	}
3.3.2.1 endTransaction
void endTransaction() {
		Assert.state(this.transactionStatus != null,
				() -> "Failed to end transaction - transaction does not exist: " + this.testContext);

		try {
			if (this.flaggedForRollback) {
        // 进入此处,调用了transactionManager的rollback方法
				this.transactionManager.rollback(this.transactionStatus);
			}
			else {
				this.transactionManager.commit(this.transactionStatus);
			}
		}
		finally {
			this.transactionStatus = null;
		}
	}

3.4 小结

大致流程如下

1. 执行beforeTestMethod,封装了一个TransactionContext,并把其中的flaggedForRollback设置成true,并把TransactionContext放到ThreadLocal里
2. 然后执行我们的单测方法
3. 执行afterTestMethod,从ThreadLocal里取出TransactionContext,判断里面的flaggedForRollback,如果为true,就rollback,否则就commit

在txContext.startTransaction();中,会调用transactionManager.getTransaction来获取当前的事务内容transactionStatus,关于这块的内容,可以参见这篇博客

总结

Spring这个盒子,很多实现方式都是类似的,特别这个SPI机制,很多地方使用,也是Spring框架开放给第三方框架用来接入的一个大杀器。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值