结合Junit的extension机制、Mockito机制可以构造端到端用例体系
启动spring框架
spring托管主测试类
通过@ExtendWith(SpringExtension.class)
和@ContextConfiguration(classes = ApplicaitonConfig.class)
,以及补充Spring上下文环境类可以实现主测试类spring环境启动和注入
@Configuration
@ComponentScan("project.myproject.test")
public class ApplicaitonConfig {
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ApplicaitonConfig.class)
public class MainTest {
@Autowired
private TestBean1 bean1;
@Test
public void lookuptest() throws InterruptedException {
……
}
}
实现spring环境启动后置处理器
如果有需要mockbean,或者在环境启动后做一些特殊处理的话,可以实现TestExecutionListener类
public class SpringTestListener implements TestExecutionListener {
@Getter
private static ApplicationContext applicationContext;
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
ApplicationContext applicationContext = testContext.getApplicationContext();
SpringTestListener.applicationContext = applicationContext;
……
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
……
}
}
获取到applicationContext后,就可以通过getBean获取到对应的bean
如果对某个spring托管bean中的某个字段有打桩的需求,可以实现自己的Test类,通过getBean获取到这个bean,并通过字段的set方法将自己的Test类配置到bean中
如果有其他环境配置需求,例如初始化数据库表,都可以在beforeTestClass
方法中完成操作
实现spring环境只启动一次
使用@ExtendWith
注解的问题在于,每个注解类都会执行一次Extesnion,即有多个测试类的情况下,spring环境会启停多次
可以通过在pom中增加插件,配置reuseForks属性实现
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>true</reuseForks>
<trimStackTrace>false</trimStackTrace>
</configuration>
</plugin>
resuseForks为true,会重用环境,为false,每个测试类将清理环境并重新启动jvm
启动内存中间件
启动内存数据库
借助mariadb组件可以实现内存数据库启动,如果配合spring,可以实现datasource在spring的托管(如果服务本身使用spring托管jdbc的话)
注册mariadb内存数据库bean
使用Configuration类注册内存数据库相关的bean(或者直接使用@Component注解注册)
@Configuration
@ComponentScan(basePackages = {"demo.gty.test"})
public class ApplicationConfig {
/**
* 启动mariadb内存数据库服务,并将该服务作为bean托管
*/
@Bean
public MariaDB4jSpringService mariaDB4jSpringService() throws IOException {
MariaDB4jSpringService mariaDB4jSpringService = new MariaDB4jSpringService();
mariaDB4jSpringService.setDefaultPort(sqlPort);
mariaDB4jSpringService.getConfiguration().addArg("--character-set-server=utf8mb4");
mariaDB4jSpringService.getConfiguration().addArg("--collation-server=utf8mb4_general_ci");
mariaDB4jSpringService.getConfiguration().addArg("--user=root");
mariaDB4jSpringService.getConfiguration().addArg("--enable-lower_case_table_names");
return mariaDB4jSpringService;
}
/**
* 在内存数据库中创建DB,并使用德鲁伊连接建立数据连接,返回这个连接
*/
@Bean
public DruidDataSource myDb(MariaDB4jSpringService mariaDB4jSpringService) throws ManagedProcessException {
mariaDB4jSpringService.getDB().createDB("memorydb01");
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("");
dataSource.setUrl(getURL("udrmetric01db", config.getPort()) + "?allowMultiQueries=true");
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
return dataSource;
}
}
这样,我们获得了一个spring托管的数据库连接,如果有必要,可以注册JdbcTemplate的托管,只需要注入这个datasource即可
<bean id="memoryJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="myDb" />
</bean>
因为MariaDB4jSpringService已经实现了LifeCycle接口,不需要我们自行去管理它的启停
public class MariaDB4jSpringService extends MariaDB4jService implements Lifecycle
初始化数据库脚本
当我们获取到了jdbcTemplate的bean,初始化脚本就很轻松了,在上面spring启动部分有一个实现了TestExecutionListener#beforeTestClass
的上下文启动后置处理器,在该方法中嵌入jdbcTemplate.execute()
完成sql脚本初始化即可。
或者还有一个办法,使用spring完成脚本初始化
<jdbc:initialize-database data-source="myDb">
<jdbc:script location="classpath:init/table_memorydb01.sql" />
</jdbc:initialize-database>
启动内存redis
基于embedded-redis组件可以实现内存redis部署
注册内存redis相关bean
使用Configuration类或@Component注解可以向spring中注册内存redis相关bean的托管
@Component
public class MemoryRedisServer extends RedisServer {
private static final String DEFAULT_PORT = "17378";
private static final String DEFAULT_SIZE = "256m";
private static final String MAX_CLIENTS = "15000";
public MemoryRedisServer() throws IOException {
super(Integer.parseInt(DEFAULT_PORT));
File executable = RedisExecProvider.defaultProvider().get();
if (SystemUtils.IS_OS_WINDOWS) {
args = Arrays.asList(executable.getAbsolutePath(), "--port", DEFAULT_PORT, "--requirepass", CommonTestArg.DEFAULT_AUTH,
"--maxheap", DEFAULT_SIZE, "--maxclients", MAX_CLIENTS);
} else {
args = Arrays.asList(executable.getAbsolutePath(), "--port", DEFAULT_PORT, "--requirepass", CommonTestArg.DEFAULT_AUTH,
"--maxmemory", DEFAULT_SIZE, "--maxclients", MAX_CLIENTS);
}
}
}
由于embedded-redis组件没有像mariadb一样适配Spring的LifeCyle,因此我们需要自行实现Lifecycle完成redisServer在spring中的生命周期管理
@Component
public class RedisServerStartup implements SmartLifecycle {
private volatile boolean isRunning = false;
@Autowired
private MemoryRedisServer memoryRedisServer;
@Autowired
private RedisClient redisClient;
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public void start() {
if (!memoryRedisServer.isActive()) {
memoryRedisServer.start();
}
isRunning = true;
try {
doSomeMock();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void stop() {
isRunning = false;
}
@Override
public void stop(Runnable callback) {
stop();
metricRedisServer.stop();
callback.run();
}
@Override
public boolean isRunning() {
return isRunning;
}
}
可以看到,实现redis启停的同时,还可以向其中注入我们的redisClient,完成一些redis数据的初始化doSomeMock()
至于redisClient的注册,方法就很多了,比如直接拿内存redis的配置主动创建一个并注册到spring,或者通过修改环境变量,从环境变量中拿取配置并创建等等
启动内存sftp
有时服务涉及sftp,可能需要启动内存sftp,基于sshd-core组件可以实现内存SFTP服务器启动
构建服务器
public class SftpServerTestSupport {
public static final int port = RandomUtils.getInt(1000) + 5000;
@Getter
private SshServer sshd;
// 启动sftp服务器
public int start() throws Exception {
setupTestServer();
sshd.start();
return sshd.getPort();
}
// sftp服务器配置类
private KeyPairProvider createTestHostKeyProvider(Class<?> anchor) {
Path targetFolder = getTargetFolder();
Path file = targetFolder.resolve("hostkey." + KeyUtils.EC_ALGORITHM.toLowerCase(Locale.ROOT));
return createTestHostKeyProvider(file);
}
// sftp服务器配置类
private Path getTargetFolder() {
String basedir = System.getProperty("basedir");
Path targetFolder = Paths.get(basedir, "target");
return targetFolder;
}
// sftp服务器配置类
private KeyPairProvider createTestHostKeyProvider(Path path) {
SimpleGeneratorHostKeyProvider hostKeyProvider = new SimpleGeneratorHostKeyProvider();
hostKeyProvider.setAlgorithm(KeyUtils.EC_ALGORITHM);
hostKeyProvider.setKeySize(256);
hostKeyProvider.setPath(Objects.requireNonNull(path, "No path"));
return hostKeyProvider;
}
// 配置sftp服务器
private SshServer setupTestServer() {
sshd = SshServer.setUpDefaultServer();
sshd.setPort(SftpServerTestSupport.port);
sshd.setKeyPairProvider(createTestHostKeyProvider(getClass()));
sshd.setPasswordAuthenticator(new MyPasswordAuthenticator());
sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
sshd.setCipherFactories(Arrays.asList(BuiltinCiphers.values()));
sshd.setShellFactory(new InteractiveProcessShellFactory());
sshd.setCommandFactory(new ProcessShellCommandFactory());
sshd.setFileSystemFactory(new MemorySftpFileSystemFactory());
SftpSubsystemFactory subsystemFactory = new SftpSubsystemFactory.Builder()
.withExecutorServiceProvider(() ->
ThreadUtils.noClose(new SshThreadPoolExecutor(5, 5,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20))))
.build();
sshd.setSubsystemFactories(Collections.singletonList(subsystemFactory));
CoreModuleProperties.NIO2_READ_TIMEOUT.set(sshd, Duration.ZERO);
return sshd;
}
public void stop() throws IOException {
sshd.stop();
}
public boolean isStarted() {
return sshd.isStarted();
}
// 自定义一个密码校验器
class MyPasswordAuthenticator implements PasswordAuthenticator {
private final Map<String, String> PASSWORDS = ImmutableMap.of("rkey", "rkeypass",
"service", "servicepassword");
@Override
public boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException, AsyncAuthException {
return password.equals(PASSWORDS.get(username));
}
}
}
通过sshd组件构造了一个SshdServer,这样就基于127.0.0.1:port启动了一个服务器,对外暴露了start()
方法,可选的启动方式就很多了,例如基于Spring的TestExecutionListener#beforeTestClass
启动,或者直接基于@ExtendWith和Extension机制启动
配置环境变量
使用任何一种前置执行器插入方式,例如基于Spring的TestExecutionListener#beforeTestClass
都可以实现环境变量的配置。环境变量的配置主要是通过反射找到正确的成员变量,将自定义的环境变量存入其中
private static void setEnv() throws Exception {
HashMap<String, String> newEnv = new HashMap<>(System.getenv());
// 取出当前的系统环境变量,封装成map,并做对应处理
doSomethingForNewEnv(newEnv);
// 把处理后的环境变量通过反射的反射存入对于的成员变量中
try {
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
Class<?> processEnvClass = Class.forName("java.lang.ProcessEnvironment");
Field envField = processEnvClass.getDeclaredField("theEnvironment");
for (Field each : fields) {
if ("theEnvironment".equals(each.getName())) {
envField = each;
}
}
envField.setAccessible(true);
Map<String, String> env = (Map<String, String>) envField.get(null);
env.putAll(newenv);
Field caseInsensitiveEnvField = processEnvClass.getDeclaredField("theCaseInsensitiveEnvironment");
caseInsensitiveEnvField.setAccessible(true);
Map<String, String> cienv = (Map<String, String>) caseInsensitiveEnvField.get(null);
cienv.putAll(newenv);
} catch (NoSuchFieldException e) {
Class[] declaredClasses = Collections.class.getDeclaredClasses();
Map<String, String> env = System.getenv();
for (Class clz : declaredClasses) {
if ("java.util.Collections$UnmodifiableMap".equals(clz.getName())) {
Field field = clz.getDeclaredField("m");
field.setAccessible(true);
Object obj = field.get(env);
Map<String, String> map = (Map<String, String>) obj;
map.clear();
map.putAll(newenv);
}
}
}
}
如果业务需要的是System.getProperty()
配置项,就比较简单可以直接使用System#setProperty
完成配置,或通过打桩
跨线程mock技术实现
在端到端测试流程中,服务难免会出现起多线程的情况,会导致mock失效 ,可以参考跨线程mock,进行多线程间的mock同步
跨线程mock代码插入可以在TestExecutionListener#beforeTestClass
的实现类中进行
把日志投放到控制台
通过log4j2.xml配置即可
<Configuration status="INFO" monitorInterval="60">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="……"/>
</Console>
</appenders>
<Loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</Loggers>
评论区