- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
之前分享过如何快速上手开源项目以及如何在开源项目里做集成测试,但还没有讲过具体的实操.
今天来详细讲讲如何写单元测试.
这个大家应该是有共识的,对于一些功能单一、核心逻辑、同时变化不频繁的公开函数才有必要做单元测试.
对于业务复杂、链路繁琐但也是核心流程的功能通常建议做 e2e 测试,这样可以保证最终测试结果的一致性.
我们都知道单测的主要目的是模拟执行你写过的每一行代码,目的就是要覆盖到主要分支,做到自己的每一行代码都心中有数.
下面以 Apache HertzBeat 的一些单测为例,讲解如何编写一个单元测试.
先以一个最简单的 org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl#preCheck 函数测试为例。 这里的 preCheck 函数就是简单的检测做参数校验。 测试时只要我们手动将 metrics 设置为 null 就可以进入这个 if 条件.
@ExtendWith(MockitoExtension.class)
class UdpCollectImplTest {
@InjectMocks
private UdpCollectImpl udpCollect;
@Test
void testPreCheck() {
List<String> aliasField = new ArrayList<>();
aliasField.add("responseTime");
Metrics metrics = new Metrics();
metrics.setAliasFields(aliasField);
assertThrows(IllegalArgumentException.class, () -> udpCollect.preCheck(metrics));
}
}
来看具体的单测代码,我们一行行的来看:
@ExtendWith(MockitoExtension.class) 是 Junit5 提供的一个注解,里面传入的 MockitoExtension.class 是我们单测 mock 常用的框架.
简单来说就是告诉 Junit5 ,当前的测试类会使用 mockito 作为扩展运行,从而可以 mock 我们运行时的一些对象.
@InjectMocks
private UdpCollectImpl udpCollect;
@InjectMocks 也是 mockito 这个库提供的注解,通常用于声明需要测试的类.
@InjectMocks
private AbstractCollect udpCollect;
需要注意的是这个注解必须是一个具体的类,不可以是一个抽象类或者是接口.
其实当我们了解了他的原理就能知道具体的原因:
当我们 debug 运行时会发现 udpCollect 对象是有值的,而如果我们去掉这个注解 @InjectMocks 再运行就会抛空指针异常.
因为并没有初始化 udpCollect 。
而使用 @InjectMocks注解后,mockito 框架会自动给 udpCollect 注入一个代理对象;而如果是一个接口或者是抽象类,mockito 框架是无法知道创建具体哪个对象.
当然在这个简单场景下,我们直接 udpCollect = new UdpCollectImpl() 进行测试也是可以的.
在 IDEA 中我们可以以 Coverage 的方式运行,IDEA 就将我们的单测覆盖情况显示在源代码中,绿色的部分就代表在实际在运行时执行到的地方.
我们也可以在 maven 项目中集成 jacoco,只需要添加一个根目录的 pom.xml 中添加一个 plugin 就可以了.
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
之后运行 mvn test 就会在 target 目录下生成测试报告了.
我们还可以在 GitHub 的 CI 中集成 Codecov,他会直接读取 jacoco 的测试数据,并且在 PR 的评论区加上测试报告.
需要从 Codecov 里将你项目的 token 添加到 repo 的 环境变量中即可.
具体可以参考这个 PR:https://github.com/apache/hertzbeat/pull/1985 。
刚才展示的是一个非常简单的场景,下面来看看稍微复杂的.
我们以这个单测为例: org.apache.hertzbeat.collector.collect.redis.RedisClusterCollectImplTest 。
@ExtendWith(MockitoExtension.class)
public class RedisClusterCollectImplTest {
@InjectMocks
private RedisCommonCollectImpl redisClusterCollect;
@Mock
private StatefulRedisClusterConnection<String, String> connection;
@Mock
private RedisAdvancedClusterCommands<String, String> cmd;
@Mock
private RedisClusterClient client;
}
这个单测在刚才的基础上多了一个 @Mock 的注解.
这是因为我们需要测试的 RedisCommonCollectImpl 类中需要依赖 StatefulRedisClusterConnection/RedisAdvancedClusterCommands/RedisClusterClient 这几个类所提供的服务.
单测的时候需要使用 mockito 创建一个他们的对象,并且注入到需要被测试的 RedisCommonCollectImpl类中.
不然我们就需要准备单测所需要的资源,比如可以使用的 Redis、MySQL 等.
只是注入进去还不够,我们还需要模拟它的行为:
这里以最常见的模拟函数返回为例:
String clusterNodes = connection.sync().clusterInfo();
在源码里看到会使用 connection 的 clusterInfo() 函数返回集群信息.
String clusterKnownNodes = "2";
String clusterInfoTemp = """
cluster_slots_fail:0
cluster_known_nodes:%s
""";
String clusterInfo = String.format(clusterInfoTemp, clusterKnownNodes);
Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);
此时我们就可以使用 Mockito.when().thenReturn() 来模拟这个函数的返回数据.
而其中的 cmd 自然也是需要模拟返回的:
Mockito.mockStatic(RedisClusterClient.class).when(()->RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(client);
Mockito.when(client.connect()).thenReturn(connection);
Mockito.when(connection.sync()).thenReturn(cmd);
Mockito.when(cmd.info(metrics.getName())).thenReturn(info);
Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);
cmd 是通过 Mockito.when(connection.sync()).thenReturn(cmd);返回的,而 connection 又是从 client.connect() 返回的.
最终就像是套娃一样,client 在源码中是通过一个静态函数创建的.
我依稀记得在我刚接触 mockito 的 16~17 年那段时间还不支持模拟调用静态函数,不过如今已经支持了:
@Mock
private RedisClusterClient client;
Mockito.mockStatic(RedisClusterClient.class).when(()->RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(client);
这样就可以模拟静态函数的返回值了,但前提是返回的 client 需要使用 @Mock 注解.
有时候我们也需要模拟构造函数,从而可以模拟后续这个对象的行为.
MockedConstruction<FTPClient> mocked = Mockito.mockConstruction(FTPClient.class,
(ftpClient, context) -> {
Mockito.doNothing().when(ftpClient).connect(ftpProtocol.getHost(),
Integer.parseInt(ftpProtocol.getPort()));
Mockito.doAnswer(invocationOnMock -> true).when(ftpClient)
.login(ftpProtocol.getUsername(), ftpProtocol.getPassword());
Mockito.when(ftpClient.changeWorkingDirectory(ftpProtocol.getDirection())).thenReturn(isActive);
Mockito.doNothing().when(ftpClient).disconnect();
});
可以使用 Mockito.mockConstruction 来进行模拟,该对象的一些行为就直接写在这个模拟函数内.
需要注意的是返回的 mocked 对象需要记得关闭.
当然也不是所有的场景都需要 mock.
比如刚才第一个场景,没有依赖任何外部服务时就不需要 mock.
类似于这个 PR 里的测试,只是依赖一个基础的内存缓存组件,就没必要 mock,但如果依赖的是 Redis 缓存组件还是需要 mock 的。 https://github.com/apache/hertzbeat/pull/2021 。
如果有些测试场景下需要获取内部变量方便后续的测试,但是该测试类也没有提供获取变量的函数,我们就只有修改源码来配合测试了.
比如这个 PR:
当然如果只是给测试环境下使用的函数或变量,我们可以加上 @VisibleForTesting注解标明一下,这个注解没有其他作用,可以让后续的维护者更清楚的知道这是做什么用的.
单元测试只能测试一些功能单一的函数,要保证整个软件的质量仅依赖单测是不够的,我们还需要集成测试.
通常是需要对外提供服务的开源项目都需要集成测试:
以我接触到的服务型应用主要分为两类:一个是 Java 应用一个是 Golang 应用.
Golang 因为工具链没有 Java 那么强大,所以大部分的集成测试的功能都是通过编写 Makefile 和 shell 脚本实现的.
还是以我熟悉的 Pulsar 的 go-client 为例,它在 GitHub 的集成测试是通过 GitHub action 触发的,定义如下: 最终调用的是 Makefile 中的 test 命令,并且把需要测试的 Golang 版本传入进去.
Dockerfile:
这个镜像简单来说就是将 Pulsar 的镜像作为基础运行镜像(这里面包含了 Pulsar 的服务端),然后将这个 pulsar-client-go 的代码复制进去编译.
接着运行:
cd /pulsar/pulsar-client-go && ./scripts/run-ci.sh
也就是测试脚本.
测试脚本的逻辑也很简单:
localhost
,所以可以直接连接。通过这里的 action 日志可以跟踪所有的运行情况.
Java 因为工具链强大,所以集成测试几乎不需要用 Makefile 和脚本配合执行.
还是以 Pulsar 为例,它的集成测试是需要模拟在本地启动一个服务端(因为 Pulsar 的服务端源码和测试代码都是 Java 写的,更方便做测试),然后再运行测试代码.
这个的好处是任何一个单测都可以在本地直接运行,而 Go 的代码还需要先在本地启动一个服务端,测试起来比较麻烦.
来看看它是如何实现的,我以其中一个 BrokerClientIntegrationTest为例: 会在单测启动的时候先启动服务端.
最终会调用 PulsarTestContext 的 build 函数启动 broker(服务端),而执行单测也只需要使用 mvn test 就可以自动触发这些单元测试。 只是每一个单测都需要启停服务端,所以要把 Pulsar 的所有单测跑完通常需要 1~2 个小时.
以上就是日常编写单测可能会碰到的场景,希望对大家有所帮助.
最后此篇关于深入理解单元测试:技巧与最佳实践的文章就讲到这里了,如果你想了解更多关于深入理解单元测试:技巧与最佳实践的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是新手。查看 Google 新闻...上下滚动页面时请注意左侧导航栏。 看看它是如何滚动一点,然后在它消失之前粘在页面顶部的? 关于如何做到这一点有什么想法吗? jQuery 和 CSS 可以复制吗
技巧 1:在 Web 服务器上缓存常用数据 技巧 2:在 Application 或 Session 对象中缓存常用数据 技巧 3:在 Web 服务器磁盘上缓存数据和 HTML 技巧 4:避免
我在 excel 中有一个电子表格,其中包含以下行: COLUMN Value1.Value2.Value3 Value4.Value5.Value6 Value7.Value8.Val
GNU Makefile 中是否有任何技巧来获取规则的所有依赖项? 例子: rule1: dep1_1 dep1_2 dep1_3 rule2: dep2_1 dep2_2 rule1 dump_
人们使用什么来追踪内存泄漏?我已经通过代码检查设法解决了一些问题,但我不知道下一步该做什么/当我的程序变大时我将如何管理问题。我知道我在泄漏什么类型的对象,但我不知道是什么让它保持活力。 在 Wind
有什么好的方法可以将“xlSum”、“xlAverage”和“xlCount”等字符串转换为它们在 Microsoft.Office.Interop.Excel.XlConsolidationFunc
我们都见过这个: javascript:document.body.contentEditable='true'; document.designMode='on';无效 0 但我的问题是,这实际上是
我的应用程序将输出一个图形,其布局由用户定义。自定义布局类应该实现我定义的接口(interface)。我应该怎么做?有一个特殊的文件夹,我可以在其中查找布局类?用户是否将类名作为参数传递给应用? 如有
我在弄清楚如何在 Javascript 中自引用表行时遇到了一些麻烦。 这是简化的代码: $( "#listitems tbody" ).append( "" + "" + id.va
关闭。这个问题需要更多focused .它目前不接受答案。 想改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭 6 年前。 Improve this q
我正在将代码库从一种编程风格转移到另一种编程风格。 我们有一个名为 Operand 的类型,定义如下: class Operand {...}; 然后我们有 class OperandFactory
我使用以下缩略图类在我的内容包装器中显示 4x3 缩略图: .thumbnail { float:left; width:300px; height:200px; ma
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the
我认为这是不可能的,但我想在放弃之前问问你。 我想要类似 constexpr 增量的东西。 #include constexpr int inc() { static int inc = 0;
是否有任何适合 C++ 新手的技术和描述的好列表。我在想一个描述 RAII、RVO、左值的列表……这适用于目前不了解这些技术或来自不适用这些技术的其他语言的新手。 最好是短小精悍的:-) 最佳答案 是
我有一个二进制字符串 '01110000',我想在不编写 forloop 的情况下返回前面的前导零数。有谁知道如何做到这一点?如果字符串立即以“1”开头,最好也返回 0 最佳答案 如果您真的确定它是一
我需要优化我的应用程序的 RAM 使用率。 请省去那些告诉我在编写 Python 代码时不应该关心内存的讲座。我有内存问题,因为我使用非常大的默认字典(是的,我也想快点)。我目前的内存消耗是 350M
有时,当我看到一个我喜欢的网站或来自受人尊敬的人的网站时,我会查看源代码并尝试理解它们(就像我们所有人一样)。 关于 Jeremy Keiths他使用以下代码的网站: [role="navigatio
这是我怎样设置 Git 来管理我的家目录的方法。 我有好几台电脑。一台笔记本电脑用于工作,一台工作站放在家里,一台树莓派(或四台),一台 Pocket CHIP,一台 运行
shell 技巧 表变量 HBase 0.95 版本增加了为表提供 jruby 风格的面向对象引用的 shell 命令。以前,作用于表的所有 shell 命令都具有程序风格,该风格始终将表的名称作
我是一名优秀的程序员,十分优秀!