导读
阅读此文需要有基础的 maven、docker、GithubAction 使用经验
作为一个开源项目,不比公司项目迭代会有专门的测试童鞋跟进,想要保证代码质量,更多的是依靠编码的人员的技术水平和自动化测试,Dubbo 也不例外。如果准备成为一个 Dubbo Contributor,花点时间了解下 Dubbo 的自动化测试体系,也是很有必要的。
整体来看,Dubbo 的测试代码分为两部分,一部分是在 apache/dubbo 的单元测试,另一部分是在 apache/dubbo-samples 的集成测试,从功能层面来看,前者关注单个函数功能是否正常,后者关注 Provider/Consumer 两端交互是否符合预期。
接下来,我们将讨论下常见的测试用例写法注意点,以及介绍部分背后的技术点。
单元测试
Dubbo 的单元测试使用的是 Junit 5,具体测试语法并无特殊,读者可自行参阅相关使用文档。
相比于集成测试,单元测试是从开发的角度在函数粒度对功能进行测试,针对函数的上下文依赖,通常通过 mock 的方式进行注入。下面将介绍几个在 Dubbo 中编写单元测试的注意点,大多是通用共识。
测试用例的编写需要始终牢记一个原则:在满足必要功能覆盖的情况下,尽量克制地占用执行时间和硬件资源。
因为当一个项目的测试用例运行时间达到小时级别的时候,开发人员由于得不到及时的反馈,很有可能最后选择绕过。
慎用 *Each 注解
特指 @BeforeEach
、@AfterEach
,被该注解标记的方法,会在同一测试类内所有 @Test
, @RepeatedTest
, @ParameterizedTest
, @TestFactory
标记的方法前后重复执行。
所以在日常实践中,只会在 Each 方法里放置轻量短耗时操作,再则一些公用的初始化和销毁操作可以使用@BeforeAll
和 @AfterAll
替代,注意 All 系列方法一般要求是静态方法,虽然在 per-class 模式下可以是实例方法,但该模式过于消耗性能,不建议轻易使用。
举个例子,我们需要测试 Zookeeper 的一些功能方法,可以复用同一个 zkServer
来避免频繁创建销毁带来的性能损耗,在每个 testMethod 里面,通过测试不同的路径,来防止不同方法之间互相干扰,真实案例可见源码的 Curator5ZookeeperClientTest
类。
public SampleTest {
private static TestingServer zkServer;
@BeforeAll
public static void setUp() {
// 创建 zkSever 是一个重资源操作,所以我们在整个测试类里只创建一次
zkServer = new TestingServer(2181, true);
}
@Test
public void testMethod1() {
// 依赖 zkServer 进行一些交互
}
@Test
public void testMethod2() {
// 依赖 zkServer 进行一些交互
}
@AfterAll
public static void cleanUp() {
zkServer.stop();
}
}
可能阻塞的操作请加 Timeout
列觉几个常见的场景:
- 特别重资源的初始化操作,可能由于资源不够等原因,导致假死
- 需要连接外部服务,外部服务不受自己控制
- 存在加锁等待的情况,由于收不到释放信号量,导致死锁阻塞
同样举个小例子:
public SampleTest {
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
public void testMaybeTimeout() {
final CountDownLatch countDownLatch = new CountDownLatch(1);
oneClient.addListener(() -> {
// 依赖外部事件,进行减数
countDownLatch.countDown();
});
// 等待计数归零
countDownLatch.await();
}
}
适度使用多实例特性
Dubbo 3 中引入了多实例特性,以替换之前的大单例模型,划分了 3 类 ScopeModel:
定义 | 中文 | 备注 |
---|---|---|
FrameworkModel | 框架层 | 将需要全局缓存的进行复用(端口、序列化等) |
ApplicationModel | 应用层 | 隔离应用之间的信息,包括注册中心、配置中心、元数据中心 |
ModuleModel | 模块层 | 提供热加载能力,可以按 ClassLoader、Spring Context 进行隔离上下文 |
截止目前,详细的多实例官方文档暂未完成编写,后续视情况会进行专题介绍。
但利用多实例带来的资源隔离性,可以帮助我们写出更加健壮的单元测试来,举一个之前笔者遇到的例子,详见源码 ServiceInstanceHostPortCustomizerTest
类:
class ServiceInstanceHostPortCustomizerTest {
@Test
void customizePreferredProtocol() {
// FrameworkModel 层级共享
ApplicationModel applicationModel= new ApplicationModel(FrameworkModel.defaultModel());
applicationModel.getApplicationConfigManager().setApplication(new ApplicationConfig("service-preferredProtocol"));
// 在 ApplicationModel 层级进行资源隔离
WritableMetadataService writableMetadataService = WritableMetadataService.getDefaultExtension(applicationModel);
// 以此防止 writableMetadataService 中出现不受期望的 ServiceURL
writableMetadataService.exportURL(
URL.valueOf("tri://127.1.1.1:50052/org.apache.dubbo.demo.GreetingService")
);
}
}
该单元测试单独运行是没有问题的,但一旦和其他用例一起运行,由于 writableMetadataService
会出现不在期望内的 ServiceURL
,有一定几率该单元测试用例会执行失败。在使用 ApplicationModel 级隔离之后,我们就可以保证用例运行的确定性。
集成测试
Dubbo 的集成测试使用的是 Junit 4,两者的差别可见 JUnit 4 vs Jupiter。
相比于单元测试,集成测试是从用户的角度在功能粒度对使用流程进行测试,流程的上下文依赖,通常也是真实而非 Mock 的。同样需要牢记一个原则:在满足必要功能覆盖的情况下,尽量克制地占用执行时间和硬件资源。
编写集成测试
可之间参见 dubbo-samples/README.md,同时参考末尾 4 个示例可快速上手
注意:在常见的目录拆分中,Provier 端在 main 目录,Consumer 端在 test 目录,更多细节不做展开。
集成配置
基础用法可直接参见:Dubbo Integration Test Quick Start
这里展开说下:如何组合复用多个子模块实现多份不同的集成配置?
首先我们需要构建 maven 多模块项目,为了保证子模块的组装灵活性:
- 父 POM 的 packaging 类型建议为 pom 类型,通过 modules 标签包含多个子模块,不做依赖管理等事情
- 子模块不依赖父 POM,各自依赖配置独立,可独立运行
这样我们只需要新建一个只包含 pom.xml
、case-configuration.yml
、case-versions.conf
maven 项目,就可以实现一份新的测试用例配置,实现多模块的复用,快速构建测试场景。
示例可见:dubbo-samples-migration/dubbo-samples-migration-case-default
特例:兼容性测试
在当前设计架构下,一份 case-configuration.yml
可以同时包含多个 service 服务,最常见的搭配的是:
- 一个
type=app
的 Provider,用来提供服务 - 一个
type=test
的 Consumer,用来消费服务,并执行断言逻辑,一般基于 Junit4 执行
接下来,一份 case-configuration.yml
绑定一份 case-versions.conf
配置,也就是说所有 service 都是运行同一个 Dubbo 版本,这就导致了一些问题:
如果我想测试 Dubbo 3 对 Dubbo 2 的兼容性有没有达到设计期望 或者 我想验证 Dubbo 官方提供的 Dubbo 2 向 Dubbo 3 的迁移方案是否靠谱,该如何操作?
而这些兼容性测试,对于用户的业务稳定性来说,是必须要重点关注的事项,也是一个负责的的顶级开源软件,需要重点保障的事项。
Dubbo 的集成测试核心代码在 apache/dubbo-samples/test 目录下,接下来我们将对其进行升级,以支撑上述需求。
集成测试整体流程
- 源码编译 dubbo 项目,生成 snapshot 版本,会作为第二步的 candidateVersions(候选版本)入参。
- VersionMatcher 读取 case-versions.conf 文件,生成 -Ddubbo.version=、-Dspring.version 参数,作为 Maven 编译入参。
- 通过 maven 进行测试项目构建,此时项目中
properties
定义的版本信息会被命令行入参替换 ,同时构建时候会带上项目依赖。 - ScenarioBuilderMain 会基于 case-configuration.yml 去构建项目的 docker-compose.yml 文件,以最后提交 GithubAction 进行执行,在 docker-compose.yml 文件中,会直接挂载第 3 步中项目编译的 target 目录。
- Github workflow 触发执行。
下面列举几个使用到的核心命令:
# 打包命令
mvn --batch-mode --no-snapshot-updates --no-transfer-progress clean package dependency:copy-dependencies -DskipTests -Ddubbo.version=3.0.3-SNAPSHOT -Dspring.version=4.3.16.RELEASE
# 启动 Provider
java -classpath $(echo ./dependency/*.jar | tr ' ' ':'):./dubbo-samples-migration-default-1.0-SNAPSHOT.jar org.apache.dubbo.migration.provider.ApiProvider
# 启动单元测试
java -cp 'dubbo-test-runner.jar:./test-classes:./classes:./dependency/*' org.apache.dubbo.test.runner.TestRunnerMain ./test-classes ./classes ./dependency ./test-reports '**/*IT.class'
我们改了什么?
走了一些弯路,最后总结下兼容性测试最重要的特点:两端不能是同一个 POM,因为你不能保证你写的所有代码 Dubbo 2.0 和 3.0 都同时支持,所以必须是两个独立项目。这在日常业务的兼容性测试中,也同样适用。
我们在原有的的 case-versions.conf
配置内,新增如下配置支持:
# 老版通用配置,可以作为缺省配置,
dubbo.version=2.7*, 3.*
spring.version=4.*, 5.*
# 支持不同的 servcie 应用,配置不同的 dubbo 版本依赖,与 dubbo.version 二选一
# 为防止构建用例倍级增长,不支持配置多个版本
dubbo.provider.version=3.*
dubbo.consumer.version=2.7.*
我们主要更改了 maven 构建项目时的命令行入参以支持上述语法,主要更改在 VersionMatcher.java 文件内。
具体用法见:dubbo-samples-migration/README.md,此处不做重复描述。
总结
面对中小项目的时候,我们总是追求覆盖率,而对于大型项目来说,测试用例性能也变得重要起来,它直接影响到开发者的 DEBUG 效率,以及是否会因为过长的等待时间而选择绕过。
在 Dubbo 中,很多测试用例和功能的开发者不是同一个人,这造成了大量测试用例存在隔靴搔痒的问题,这时候尝试进行 TDD(测试驱动开发)的模式,也是个可行的可选项。
参考资料
- JUnit 4 vs Jupiter - a high-level concept & API comparison - sormuras(https://sormuras.github.io/blog/2018-09-13-junit-4-core-vs-jupiter-api.html)
- Junit 5 User guide (https://junit.org/junit5/docs/current/user-guide/)s