首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

深入理解单元测试:技巧与最佳实践

编程知识
2024年08月15日 15:06

之前分享过如何快速上手开源项目以及如何在开源项目里做集成测试,但还没有讲过具体的实操。

今天来详细讲讲如何写单元测试。

🤔什么情况下需要单元测试

这个大家应该是有共识的,对于一些功能单一、核心逻辑、同时变化不频繁的公开函数才有必要做单元测试。

对于业务复杂、链路繁琐但也是核心流程的功能通常建议做 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() 进行测试也是可以的。

🔥配合 jacoco 输出单测覆盖率


在 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

比如刚才第一个场景,没有依赖任何外部服务时就不需要 mock

类似于这个 PR 里的测试,只是依赖一个基础的内存缓存组件,就没必要 mock,但如果依赖的是 Redis 缓存组件还是需要 mock 的。
https://github.com/apache/hertzbeat/pull/2021

⚙️修改源码

如果有些测试场景下需要获取内部变量方便后续的测试,但是该测试类也没有提供获取变量的函数,我们就只有修改源码来配合测试了。

比如这个 PR

当然如果只是给测试环境下使用的函数或变量,我们可以加上 @VisibleForTesting注解标明一下,这个注解没有其他作用,可以让后续的维护者更清楚的知道这是做什么用的。

📈集成测试

单元测试只能测试一些功能单一的函数,要保证整个软件的质量仅依赖单测是不够的,我们还需要集成测试。

通常是需要对外提供服务的开源项目都需要集成测试:

  • Pulsar
  • Kafka
  • Dubbo 等

以我接触到的服务型应用主要分为两类:一个是 Java 应用一个是 Golang 应用。

🐳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

也就是测试脚本。

测试脚本的逻辑也很简单:

  • 启动 pulsar 服务端
  • 运行测试代码
    因为所有的测试代码里连接服务端的地址都是 localhost,所以可以直接连接。

通过这里的 action 日志可以跟踪所有的运行情况。

☕Java

Java 因为工具链强大,所以集成测试几乎不需要用 Makefile 和脚本配合执行。

还是以 Pulsar 为例,它的集成测试是需要模拟在本地启动一个服务端(因为 Pulsar 的服务端源码和测试代码都是 Java 写的,更方便做测试),然后再运行测试代码。

这个的好处是任何一个单测都可以在本地直接运行,而 Go 的代码还需要先在本地启动一个服务端,测试起来比较麻烦。

来看看它是如何实现的,我以其中一个 BrokerClientIntegrationTest为例:


会在单测启动的时候先启动服务端。

最终会调用 PulsarTestContextbuild 函数启动 broker(服务端),而执行单测也只需要使用 mvn test 就可以自动触发这些单元测试。

只是每一个单测都需要启停服务端,所以要把 Pulsar 的所有单测跑完通常需要 1~2 个小时。

以上就是日常编写单测可能会碰到的场景,希望对大家有所帮助。

From:https://www.cnblogs.com/crossoverJie/p/18361162
本文地址: http://shuzixingkong.net/article/1135
0评论
提交 加载更多评论
其他文章 《熬夜整理》保姆级系列教程-玩转Wireshark抓包神器教程(5)-Wireshark捕获设置
1.简介 WireShark的强大之处就在于不用你再做任何配置就可以抓取http或者https的包。今天宏哥主要是讲解和分享如何使用WireShark抓包。 2.运行Wireshark 安装好 Wireshark 以后,就可以运行它来捕获数据包了。方法如下: 1.在 Windows 的“开始”菜单中
《熬夜整理》保姆级系列教程-玩转Wireshark抓包神器教程(5)-Wireshark捕获设置 《熬夜整理》保姆级系列教程-玩转Wireshark抓包神器教程(5)-Wireshark捕获设置 《熬夜整理》保姆级系列教程-玩转Wireshark抓包神器教程(5)-Wireshark捕获设置
CH01_WPF概述
第1章:WPF概述 本章目标 了解Windows图形演化 了解WPF高级API 了解分辨率无关性概念 了解WPF体系结构 了解WPF 4.5 WPF概述 ​ 欢迎使用 Windows Presentation Foundation (WPF) 桌面指南,这是一个与分辨率无关的 UI 框架,使用基于矢
CH01_WPF概述 CH01_WPF概述 CH01_WPF概述
Sealos 就是小团队的神器
作者:阳明。Kubernetes 布道师,公众号 K8s 技术圈主理人 最近我们新开发了一个项目 fastclass.cn,这个项目是一个独立开发者的学习网站,我们的目标是帮助你使用 Figma、Python、Golang、React、VUE、Flutter、ChatGPT 等设计构建真实的应用程序
Sealos 就是小团队的神器 Sealos 就是小团队的神器 Sealos 就是小团队的神器
面试官:JDK中都用了哪些设计模式?
设计模式是前辈们经过实践验证总结的解决方案,帮助我们构建出更具可维护性、可扩展性和可读性的代码。当然,在面试的过程中,也会或多或少的被问到。那么今天,我们就来看一道设计模式中的常见面试问题:JDK 中都用了哪些设计模式? 我按照大家比较熟悉且好理解的方式,把 JDK 中使用的设计模式总结了一下,如下
面试官:JDK中都用了哪些设计模式? 面试官:JDK中都用了哪些设计模式? 面试官:JDK中都用了哪些设计模式?
CH03_布局
第3章:布局 本章目标 理解布局的原则 理解布局的过程 理解布局的容器 掌握各类布局容器的运用 理解 WPF 中的布局 WPF 布局原则 ​ WPF 窗口只能包含单个元素。为在WPF 窗口中放置多个元素并创建更贴近实用的用户男面,需要在窗口上放置一个容器,然后在这个容器中添加其他元素。造成这一限制的
CH03_布局 CH03_布局 CH03_布局
manim边学边做--圆形类
在manim的丰富图形库中,圆形类是一个基础且强大的模块。无论是简单的圆形绘制,还是复杂的圆形变换,它都能以简洁的代码实现。 manim中圆形类的相关模块主要有3个: Circle:标准的圆形 Annulus:圆环形状 Ellipse:椭圆形状 其中,Annulus和Ellipse继承自Circle
manim边学边做--圆形类 manim边学边做--圆形类 manim边学边做--圆形类
你还纠结996吗?2024年互联网公司工作时长排行榜出炉!
2024年互联网公司工作时长排行榜新鲜出炉!在这个竞争激烈的行业中,工作时长一直是人们关注的热点话题。你还在纠结996工作制吗?也许这份排行榜会给你一些意想不到的答案。 为什么一些公司依旧推行996,而另一些公司却在努力减少员工的工作时间?在工作时长与员工幸福感之间,究竟该如何找到平衡点? 在这份最
你还纠结996吗?2024年互联网公司工作时长排行榜出炉! 你还纠结996吗?2024年互联网公司工作时长排行榜出炉! 你还纠结996吗?2024年互联网公司工作时长排行榜出炉!
基于MonoGame重制《俄罗斯方块》游戏
两年前,我使用C#基于MonoGame编写了一款《俄罗斯方块》游戏,相关介绍可以参考【这篇文章】。最近,使用业余时间将之前的基于MonoGame的游戏开发框架重构了一下,于是,也就趁此机会将之前的《俄罗斯方块》游戏也重制一次,加入了许多我一直打算加入的功能,甚至包括提供跨平台的版本。 先说说这个游戏
基于MonoGame重制《俄罗斯方块》游戏 基于MonoGame重制《俄罗斯方块》游戏 基于MonoGame重制《俄罗斯方块》游戏