本教程达到的目的: 不用重启 整个spring应用程序,直接在 测试类中,就能 测试 spring的项目。
单元测试,结合 mock 测试类,可以很快的 测试某个最小模块的功能是否跟预期一致。
比如,这次我们测试 @EnableRetry 的自动重试。
在正式的项目中,方法上加注解 @Retryable ,设置好参数即可;但在mock测试中,为了方便我们使用 @Bean 生成 retryTemplate 实例的模式
前提:除了spring,还需要引入 这5个 jar
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.24.RELEASE</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>
只需要一个 test 类 ,搞定自动重试机制的完整测试
package com.wanmei.dataapi.config;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.concurrent.atomic.AtomicInteger;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.mockito.Mockito.*;
/**
* 测试 自动重试机制
*
* 参考 https://github.com/bijukunjummen/test-spring-retry/blob/master/src/test/java/retry/SpringDirectRetryTemplateTests.java
* @author stormfeng
* @date 2021-04-20 15:00
*/
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration // 不加参数,默认扫描当前包
public class RetryTest {
private static final int MAX = 5;
private static final int REAL = 5;
@Autowired
private RemoteCallService remoteCallService;
@Autowired
private RetryTemplate retryTemplate;
@Test
public void testRetry() throws Throwable {
String message = this.retryTemplate.execute(new RetryCallback<String, Throwable>() {
@Override
public String doWithRetry(RetryContext context) throws Throwable {
int i = CounterHolder.incrGet();
log.info("第 [{}] 次尝试",i);
String call;
try {
call = RetryTest.this.remoteCallService.call();
CounterHolder.remove();
} catch (Exception e) {
call = e.getClass().getName() + ": " + e.getMessage();
if (CounterHolder.get() >= MAX) {
log.error("尝试次数 [{}] 太多,最终失败了:{}", CounterHolder.get(), call);
CounterHolder.remove();
return call;
}
throw e;
}
return call;
}
});
verify(remoteCallService, times(REAL)).call(); // 校验 方法调用次数= REAL
assertThat(message, is("Completed")); // 校验 返回值
}
public interface RemoteCallService {
String call() throws Exception;
}
/**
* 计数器
* 变量是绑定到 每个线程本身的,所以不存在 线程竞争资源的问题
* 但是,要记得 最后 remove,不然框架的线程池可能会将 该线程重用,导致 结果混乱。
* @author stormfeng
* @date 2021-04-20 14:08
*/
@Slf4j
public static class CounterHolder {
private static final ThreadLocal<AtomicInteger> local = new ThreadLocal<AtomicInteger>(){
@Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
{
log.info("ThreadLocal<AtomicInteger> 初始化ok");
}
};
/**
* get & remove
*/
public static int get(){
return local.get().get();
}
public static int incrGet() {
return local.get().incrementAndGet();
}
public static void remove(){
local.remove();
}
}
/**
* @Configuration 和 @EnableRetry以及 @Bean 都能被被 @ContextConfiguration 正确扫描识别,所以能加载 到本 测试类中
* 无需启动整个 spring 项目,只加载需要测试的这几个小模块即可
*/
@Configuration
@EnableRetry
public static class SpringConfig {
@Bean
public RemoteCallService remoteCallService() throws Exception {
RemoteCallService remoteService = mock(RemoteCallService.class);
// REAL 次数 与这里保持一致 !
when(remoteService.call())
.thenThrow(new RuntimeException("Remote Exception 1"))
.thenThrow(new RuntimeException("Remote Exception 2"))
.thenThrow(new RuntimeException("Remote Exception 3"))
.thenThrow(new RuntimeException("Remote Exception 4"))
.thenThrow(new RuntimeException("Remote Exception 5"))
.thenReturn("Completed");
return remoteService;
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(500l);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(MAX);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
}
该单元测试非常紧凑,所有的类和对象都在一个test文件中搞定,灵感很大部分来源于此