目 录CONTENT

文章目录

【实践】基于Junit5搭建端到端测试用例框架

FatFish1
2024-11-07 / 0 评论 / 0 点赞 / 78 阅读 / 0 字 / 正在检测是否收录...

结合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中的生命周期管理

http://www.chymfatfish.cn/archives/applicationcontext#%E5%88%9D%E5%A7%8B%E5%8C%96%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%A4%84%E7%90%86%E5%99%A8
@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>

 

0

评论区