如何为复杂的 Java 应用编写集成测试

标签: cim test cim | 发表时间:2024-09-29 11:37 | 作者:
出处:http://crossoverjie.top/

最近有时间又把以前开源的 IM 消息系统捡起来继续开发了(确实这些年经常有朋友催更)。

没错,确实是这些年,因为上次发版还是再 2019 年的八月份。

这段时间比较重大的更新就是把 元数据中心抽离出来了,以前是和 zookeeper 的代码强耦合在一起的,重构之后可以有多种实现了。

今后甚至可以提供一个 jar 包就可以把后端服务全部启动起来用于体验,此时就可以使用一个简单的基于内存的注册中心。

除此之外做的更多的就是新增了一个集成测试的模块,没有完善的集成测试功能在合并代码的时候都要小心翼翼,基本的功能需求都没法保证。

加上这几年我也接触了不少优秀的开源项目(比如 Pulsar、OpenTelemetry、HertzBeat 等),他们都有完整的代码合并流程;首先第一点就得把测试流水线跑通过。

这一点在 OpenTelemetry 社区更为严格:

他们的构建测试流程非常多,包括单元测试、集成测试、代码风格、多版本兼容等。

所以在结合了这些优秀项目的经验后我也为 cim 项目新增相关的模块 cim-integration-test,同时也在 github 上配置了相关的 action,最终的效果如下:


“Build with Maven” 阶段触发单元测试和集成测试,最终会把测试结果上传到 Codecov,然后会在 PR 的评论区输出测试报告。

相关的 action 配置如下:

就是配置了几个 Job,重点是这里的:

1     
mvn -B package --file pom.xml     

它会编译并运行项目下面的所有 test 代码。

cim-integration-test 模块

为了方便进行集成测试,我新增了 cim-integration-test 这个模块,这里面没有任何源码,只有测试相关的代码。

类的继承关系图如下:

因为我们做集成测试需要把 cim 所依赖的服务都启动起来,目前主要由以下几个服务:

  • cim-server: cim 的服务端
  • cim-route: 路由服务
  • cim-client: 客户端

而 route 服务是依赖于 server 服务,所以 route 继承了 server,client 则是需要 route 和 server 都启动,所以它需要继承 route。

集成 test container

先来看看 server 的测试实现:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class AbstractServerBaseTest {       

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName
.parse("zookeeper")
.withTag("3.9.2");

private static final Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofSeconds(60);

@Container
public final ZooKeeperContainer
zooKeeperContainer = new ZooKeeperContainer(DEFAULT_IMAGE_NAME, DEFAULT_STARTUP_TIMEOUT);

@Getter
private String zookeeperAddr;

public void startServer() {
zooKeeperContainer.start();
zookeeperAddr = String.format("%s:%d", zooKeeperContainer.getHost(), zooKeeperContainer.getMappedPort(ZooKeeperContainer.DEFAULT_CLIENT_PORT));
SpringApplication server = new SpringApplication(CIMServerApplication.class);
server.run("--app.zk.addr=" + zookeeperAddr);
}
}

因为 server 是需要依赖 zookeeper 作为元数据中心,所以在启动之前需要先把 zookeeper 启动起来。

此时就需要使用 testcontainer 来做支持了,使用它可以在单测的过程中使用 docker 启动任意一个服务,这样在 CI 中做集成测试就很简单了。

我们日常使用的大部分中间件都是支持的,使用起来也很简单。

先添加相关的依赖:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>     
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>

然后在选择我们需要依赖的服务,比如是 PostgreSQL

1     
2
3
4
5
6
<dependency>     
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.8</version>
<scope>test</scope>
</dependency>

然后在测试代码中启动相关的服务

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CustomerServiceTest {     

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:16-alpine"
);

CustomerService customerService;

@BeforeAll
static void beforeAll() {
postgres.start();
}

@AfterAll
static void afterAll() {
postgres.stop();
}

@BeforeEach
void setUp() {
DBConnectionProvider connectionProvider = new DBConnectionProvider(
postgres.getJdbcUrl(),
postgres.getUsername(),
postgres.getPassword()
);
customerService = new CustomerService(connectionProvider);
}

通常情况下我们都是需要获取这些中间件的链接,比如 IP 端口啥的。

1     
2
org.testcontainers.containers.ContainerState#getHost     
org.testcontainers.containers.ContainerState#getMappedPort

通常是通过这两个函数来获取对应的 IP 和端口。

集成

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
@Container       
RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:7.4.0"));

public void startRoute() {
redis.start();
SpringApplication route = new SpringApplication(RouteApplication.class);
String[] args = new String[]{
"--spring.data.redis.host=" + redis.getHost(),
"--spring.data.redis.port=" + redis.getMappedPort(6379),
"--app.zk.addr=" + super.getZookeeperAddr(),
};
route.setAdditionalProfiles("route");
route.run(args);
}

对于 route 来说不但需要 zookeeper 还需要 Redis 来存放用户的路由关系,此时就还需要运行一个 Redis 的容器,使用方法同理。

最后就需要以 springboot 的方式将这两个应用启动起来,我们直接创建一个 SpringApplication 对象,然后将需要修改的参数通过 --varname=value 的形式将数据传递进去。

还可以通过 setAdditionalProfiles() 函数指定当前应用运行的 profile,这样我们就可以在测试目录使用对应的配置文件了。

image.png

1     
route.setAdditionalProfiles("route");       

比如我们这里设置为 route 就可以使用 application-route.yaml 作为 route 的配置文件启动,就不用每个参数都通过 -- 传递了。

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void login(String userName, int port) throws Exception {       
Long userId = super.registerAccount(userName);
SpringApplication client = new SpringApplication(CIMClientApplication.class);
client.setAdditionalProfiles("client");
String[] args = new String[]{
"--server.port=" + port,
"--cim.user.id=" + userId,
"--cim.user.userName=" + userName
};
client.run(args);
}

@Test
public void olu() throws Exception {
super.startServer();
super.startRoute();
this.login("crossoverJie", 8082);
this.login("cj", 8182);
MsgHandle msgHandle = SpringBeanFactory.getBean(MsgHandle.class);
msgHandle.innerCommand(":olu");
msgHandle.sendMsg("hello");
}

我们真正要测试的其实是客户端的功能,只要客户端功能正常,说明 server 和 route 也是正常的。

比如这里的 olu(oline user) 的测试流程是:

  • 启动 server 和 route
  • 登录注册两个账号
  • 查询出所有用户
  • 发送消息

最终的测试结果如下,符合预期。

image.png

碰到的问题

应用分层

不知道大家注意到刚才测试代码存在的问题没有,主要就是没法断言。

因为客户端、route、server 都是以一个应用的维度去运行的,没法获取到一些关键指标。

比如输出在线用户,当客户端作为一个应用时,在线用户就是直接打印在了终端,而没有直接暴露一个接口返回在线数据;收发消息也是同理。

其实在应用内部这些都是有接口的,但是作为一个整体的 springboot 应用就没有提供这些能力了。

本质上的问题就是这里应该有一个 client-sdk 的模块,client 也是基于这个 sdk 实现的,这样就可以更好的测试相关的功能了。

之后就准备把 sdk 单独抽离一个模块,这样可以方便基于这个 sdk 实现不同的交互,甚至做一个 UI 界面都是可以的。

编译失败

还有一个问题就是我是直接将 client/route/server 的依赖集成到 integration-test 模块中:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>       
<groupId>com.crossoverjie.netty</groupId>
<artifactId>cim-server</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.crossoverjie.netty</groupId>
<artifactId>cim-forward-route</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.crossoverjie.netty</groupId>
<artifactId>cim-client</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>

在 IDEA 里直接点击测试按钮是可以直接运行这里的测试用例的,但是想通过 mvn test 时就遇到了问题。

image.png

会在编译期间就是失败了,我排查了很久最终发现是因为这三个模块应用使用了springboot 的构建插件:

1     
2
3
4
5
6
7
8
9
10
11
<plugin>     
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

这几个模块最终会被打包成一个 springboot 的 jar 包,从而导致 integration-test 在编译时无法加载进来从而使用里面的类。

暂时没有找到好的解决办法,我就只有把这几个插件先去掉,需要打包时再手动指定插件。

1     
mvn clean package spring-boot:repackage -DskipTests=true     

其实这里的本质问题也是没有分层的结果,最好还是依赖 routeserver 的 SDK 进行测试。

现在因为有了测试的 CI 也欢迎大家来做贡献,可以看看这里的 help want,有一些简单易上手可以先搞起来。

https://github.com/crossoverJie/cim/issues/135

参考链接:

相关 [复杂 java 应用] 推荐:

如何为复杂的 Java 应用编写集成测试

- - crossoverJie's Blog
最近有时间又把以前开源的 IM 消息系统捡起来继续开发了(确实这些年经常有朋友催更). 没错,确实是这些年,因为上次发版还是再 2019 年的八月份. 这段时间比较重大的更新就是把 元数据中心抽离出来了,以前是和 zookeeper 的代码强耦合在一起的,重构之后可以有多种实现了. 今后甚至可以提供一个 jar 包就可以把后端服务全部启动起来用于体验,此时就可以使用一个简单的基于内存的注册中心.

Java应用运维

- - BlueDavy之技术blog
对于互联网产品或长期运行的产品而言,运维工作非常重要,尤其是在产品复杂了以后,在这篇blog中就来说下Java应用的运维工作(ps:虽然看起来各种语言做的系统的运维工作都差不多,但细节上还是会有很多不同,so本文还是只讲Java的). 苦逼的码农按照需求开发好了一个全新的Java Web应用,该发布上线给用户用了,要把一个Java Web应用发布上线,首先需要搭建运行的环境,运行的环境需要有JDK、APPServer,在已经装好了os的机器上装上JDK和APPServer,开发好的Java Web应用可以用maven直接打成war或ear,将这个打好的包scp或其他方式到目标机器上,准备妥当,就差启动了.

Java线程池应用

- - CSDN博客架构设计推荐文章
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务. 2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机). Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具.

Java 应用一般架构

- - SegmentFault 最新的文章
原文地址: https://blog.coding.net/blog/General-architecture-for-Java-applications. 当我们架设一个系统的时候通常需要考虑到如何与其他系统交互,所以我们首先需要知道各种系统之间是如何交互的,使用何种技术实现. 现在我们常见的不同系统不同语言之间的交互使用WebService,Http请求.

xssProject在java web项目中应用

- - Java - 编程语言 - ITeye博客
1.项目引入xssProtect-0.1.jar、antlr-3.0.1.jar、antlr-runtime-3.0.1.jar包. * 覆盖getParameter方法,将参数名和参数值都做xss过滤. * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取
.

使用Gradle构建Java Web应用(译)

- - BlogJava-首页技术区
使用Gradle构建Java Web应用. 本文是发布在 java.net上的一篇摘自于一书中的 节选,介绍了使用 Gradle构建Java Web应用的过程. 刚刚接触Gradle,看到了这篇小文,随手译了出来:-) (2014.01.23最后更新). 在职业生涯和私人生活中,我们中间的许多人要同时管理多个项目.

Java-了解注解及其应用

- - BlogJava-qileilove
  注解就是一种标记,在程序中加了注解就等于加了标记,没加,就没有标记. java编译器、开发工具或是其他程序可以通过反射技术了解你的类或各种元素是否有标记,有什么标记就做什么. 比如:子类重写父类的方法,方法上必须有@override标记;若一个方法已过时不用了,就该方法添加注.   解@Deprecated,调用者反射时就明白这方法已过时.

消除Java应用中的Exception开销

- - 舒の随想日记
抛异常最大的消耗在于构造整个异常栈的过程,如果你的栈很深,特别是用了一些框架的话,这个开销基本是不可忽视的,之前做的一个优化显示当时应用中的一个异常使得整个应用的性能下降至少30%. 最大开销的地方在这里,当你去new一个Exception的时候,会调用父类Throwable的构造函数, Throwable的构造函数中会调用native的fillInStackTrace(),这个方法就会构造整个异常栈了.

JAVA 应用性能监控基础

- - Linux - 操作系统 - ITeye博客
       这里简单介绍了JAVA 应用程序部署linux 服务器上的一些常用监控信息,虽然现在很多自动化监控的东西,但是一些基本的东西,我们还是需要了解.        1.我习惯性先看看 CPU 和内存的使用情况,做一个简单的关注.           命令:top 可以关注运行状态.           命令:大写P:按CPU 使用排序,大写M:按内存使用排序,小写c:详细显示应用       .

Java应用线上排查总结

- - arccode
本文总结了一些常见的线上应急现象和对应排查步骤和工具. 分享的主要目的是想让对线上问题接触少的同学有个预先认知,免得在遇到实际问题时手忙脚乱. 毕竟作者自己也是从手忙脚乱时走过来的. 在线上应急过程中要记住,只有一个总体目标: 尽快恢复服务,消除影响. 不管处于应急的哪个阶段,我们首先必须想到的是恢复问题,恢复问题不一定能够定位问题,也不一定有完美的解决方案,也许是通过经验判断,也许是预设开关等,但都可能让我们达到快速恢复的目的,然后 保留部分现场,再去定位问题、解决问题和复盘.