“单元测试”应该怎么写比较好

news2024/12/24 11:39:47

如何正确写单元测试

  • 单元测试重要性
  • 写单元测试时存在的问题
  • 1、如何命名测试类&方法
    • 1.1、测试类命名规范
    • 1.2、测试方法命名规范
  • 2、测试类的要求
    • 2.1测试行覆盖率100%
    • 2.2、单一职责
    • 2.3、可重复
    • 2.4、外部隔离,无任何外部依赖
    • 2.5、正确的使用断言
    • 2.6、不应该为了测试方便修改线上代码
    • 2.7、线上bug应该沉淀为测试用例
    • 2.8、快速原则
  • 3、选择测试框架
    • 3.1、基础测试框架
    • 3.2、如何在外部隔离的前提下测试DAO层
  • 4、如何获得测试覆盖率
    • 4.1、使用idea工具获取测试覆盖率情况
  • 5、关于 PowerMockito 工具的简单 demo
    • 5.1、使用powermockito,在test类名上使用
    • 5.2、普通对象的mock
    • 5.3、静态方法的mock
    • 5.4、静态方法的void方法mock,模拟抛出异常
    • 5.5、模拟测试构造函数
    • 5.6、如何测试是否打印了日志
    • 5.7、测试预期抛出异常
    • 5.8、注入依赖

单元测试重要性

微软公司之前有这样一个统计:bug在单元测试阶段被发现的平均耗时是3.25小时,如果遗漏到系统测试则需要11.5个小时。由此可见单元测试的重要性。

写单元测试时存在的问题

虽然单元测试很重要,但是在工作中还是会发现不少同学在书写单元测试时,存在许多问题,我将常见的问题总结如下:

  • 依赖了SpringBootTest框架,由于过长的启动耗时,导致代码单测的代价很大,这就极大限制了把单测作为一个日常态运行。
  • 不可重复测试,常见于写数据接口,往往写入后由于数据的唯一性检验,导致测试用例在每次测试前都需要该。
  • 覆盖度不够,往往只测试自己想验证的代码分支,而忽略了其他重要的的代码逻辑。
  • 无从下手写,有些类有着比较复杂的静态和环境的依赖,无从测起。
  • 缺乏断言,执行完测试用例无法明确是否验证了逻辑

下面我写一个单元测试的demo,并介绍常用的powermock框架。

一个测试用例的demo:

@RunWith(PowerMockRunner.class)
@PrepareForTest(PrivatePartialMockingExample.class)
public class PrivatePartialMockingExampleTest{
	@Test
	public void demoPrivateMethodMocking() throws Exception {
        final String expected = "TEST VALUE";
        final String nameOfMethodToMock = "methodToMock";
        final String input = "input";
        PrivatePartialMockingExample underTest = PowerMockito.spy(new 		PrivatePartialMockingExample());
        PowerMockito.when(underTest, nameOfMethodToMock, input).thenReturn(expected);
        String actual = underTest.methodToTest();
        assertEquals(expected, actual);
        verifyPrivate(underTest).invoke(nameOfMethodToMock, input);
	}
}

1、如何命名测试类&方法

1.1、测试类命名规范

  • 测试类必须和被测试类在同一个包内
  • 测试类名字必须由被测试类类目拼接“Test”构成,比如com.test.Dummy.class,它的测试类是com.test.DummyTest.class

1.2、测试方法命名规范

当一个方法的使用场景比较复杂,为遵循单一职责原则,应考虑一个方法对应多个测试用例方法,这个时候测试方法的命名需要和被测试方法不同。

被测方法+期待行为+触发条件
例子:
boolean isChild() -------->isChildFalseAgeBiggerThan18()
int getMoney()---------->getMoneyThrowExceptionlfUserNotExist()

2、测试类的要求

2.1测试行覆盖率100%

这个是最重要的要求,既然写了测试类,它的行覆盖率要求就是 100%,没有达到==没有写测试。

另外,应该明确认知到,行覆盖率不等于测试覆盖率。根据经验,刚刚好做到 100% 行覆盖基本上整体的测试覆盖率在 10%~30%,还是远远不够的,提升质量还需要在行覆盖率的基础上,尽量做到更多的测试覆盖。

2.2、单一职责

  • 每个测试方法只针对一个方法测试,不要测试多个方法。多个测试方法之间不要有任何依赖,比如测试查询用户的方法依赖了插入用户的方法。
  • 每个测试方法的长度应做控制,不建议超过 50 行。不同的场景应尽量分拆测试方法。

2.3、可重复

在被测试方法未变化的情况下,测试用例要做到可以重复无限次调用。

Badcase:

@Test
public void dummy(){ 
    Assert.assertTrue(System.currentTimeMillis() %2==0);
}

2.4、外部隔离,无任何外部依赖

单测中应不与任何外部环境交互,不应有任何的 IO 交互,这样才能保证测试用例的成功率。

  • 如 redis 或者 db 或者外部系统 RPC 的依赖应尽量使用本地的 db 或者 mock 的 bean 模拟,避免跑测试用例的时候因为外部系统不稳定或者网络不通无法测试。
  • 数据库可以使用 in memory db 模拟,java 可以使用 h2database。

2.5、正确的使用断言

  • 没有断言等于没有写测试。每个测试方法必须有至少一个断言语句。
  • 即使没有返回的方法,可以来校验日志打印是否被正确打印了,或者方法是否没有出异常。
  • 应多使用非 expect true 的方式,这样便于查看出错信息。

2.6、不应该为了测试方便修改线上代码

Badcase:

//...something upper
if(!TestContext.isUnitTest()){ 
} 
//...something down
DummyService.doRealThing();

2.7、线上bug应该沉淀为测试用例

每个 bug 修复上线后,应有避免出现类似问题的单元测试,确保下次不会出同样问题。

2.8、快速原则

每个单元测试类的耗时不应大于 100ms,为了能快速对整个项目测试,应控制测试类的依赖、复杂度,提升运行速度,这样才能做到测试常态化。

3、选择测试框架

3.1、基础测试框架

单元测试框架最常用的是 JUnit4/5。但在现实场景中,JUnit 往往力不从心,因为要满足外部隔离和快速测试的要求。对于代码中的静态依赖、final、spring bean 依赖等情况,在不真实启动容器或者面对巨大静态依赖的前提下,快速地将待测试的逻辑充分测试,这就要用到一个比较好用的工具:PowerMock

  • PowerMock 扩展自 Mockito,通过 Java 反射机制解决了 Mockito 的一些问题,并通过提供定制的类加载器以及一些字节码改写技巧的应用。PowerMock 在 Mockito 基础上实现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大功能,是实现外部隔离和快速原则的利器。

3.2、如何在外部隔离的前提下测试DAO层

由于 mybatis 集成度较高,需基于 SpringBootTest 前提下测试,但真实数据库可用 H2 Database 做替代,避免远程 IO 交互,不违背外部隔离原则。

  • 新建test-application.properties文件,配置 H2 Database 的基本信息和 MyBatis 的 mapper location:
# mysql 驱动:h2
spring.datasource.driver-class-name=org.h2.Driver
# h2 内存数据库库名:test
spring.datasource.url=jdbc:h2:mem:test
#初始化数据表
spring.datasource.schema=classpath:init_table.sql
spring.datasource.username=
spring.datasource.password=
# 打印 SQL 语句, Mapper 所处的包
logging.level.com.hawkingfoo.dao=debug
#放 mapper 的地方
mybatis.mapper-locations=classpath:/sqlmaps/*.xml
  • 新建init_table.sql文件,文件名与test-application.properties文件中的spring.datasource.schema行的值一致。注意创建表语句中应去除最后一行ENGINE=XXX,否则 H2 Database 执行时会报符号错误。例如创建student表的语句:
DROP TABLE IF EXISTS 'student';
CREATE TABLE'student'( )ENGINE=InoDB DEFAULT CHARSET=utf8mb4; PRIMARY KEY('id') id' int(10) unsigned NOT NULL AUTO_INCREMENT, name' varchar(1024) NOT NULL, sex'tinyint(1) NOT NULL, addr' varchar(1024) NOT NULL,
  • 在测试目录下增加 Spring Boot 的启动类DaoTestSpringBootAppication,注意配置scanBasePackages,只扫描 DAO 相关类,避免初始化无关的类。
@SpringBootApplication(scanBasePackages="com.onx.buyerhome.service.infra.db")
@PropertySource("classpath:test-application.properties")
public class DaoTestSpringBootApplication{
    public static void main(String[] args){
        SpringApplication.run(DaoTestSpringBootApplication.class, args);
    }
}
  • 新建Base Test类,用于test类继承使用
@RunWith(SpringRunner.class)
public class DaoTestBase {
}
  • 最后可以愉快的写Dao的测试用例了
public class BuyerPlanMapperTest extends DaoTestBase {
    @Resource
    BuyerPlanMapper buyerPlanMapper;

    @Test
    public void queryByIds(){
        List<Plano> plans=buyerPlanMapper.querybyIds(Lists.newArrayList(101L));
        Assert.assertTrue(plans.size()>0);
        Assert.assertEquals(Long.valueOf(101L), plans.get(0).getId());
    }
}

4、如何获得测试覆盖率

4.1、使用idea工具获取测试覆盖率情况

可以直接使用idea的覆盖率工具来查看测试用例的覆盖情况

  • 在测试类名上弹开右键菜单,选择使用覆盖率运行。
  • 查看单个类的总体覆盖情况。
  • 查看类里面具体行的覆盖情况,左边显示为绿色的即为覆盖的行,为红的即为没有覆盖到。

5、关于 PowerMockito 工具的简单 demo

列举了一些常见情况下 PowerMockito 的用法,实际使用中若有其他疑惑可自行搜索工具检索。

5.1、使用powermockito,在test类名上使用

@RunWith(PowerMockRunner.class)

@RunWith(PowerMockRunner.class)
public class RpcClientAopLogTest{
}

5.2、普通对象的mock

public class RpcClientAopLogTest{
    CommonsConfigHolder commonsConfigHolder;

    @Before
    public void setUp(){
        commonsConfigHolder = PowerMockito.mock(CommonsConfigHolder.class);
        PowerMockito.when(commonsConfigHolder.getCommonsConfig()).thenReturn(new Comm());
    }
}

5.3、静态方法的mock

    public void setUp(){
        PowerMockito.mockStatic(ProfilerUtil.class);
        PowerMockito.when(ProfilerUtil.getCurrentUid()).thenReturn(1L);
    }

5.4、静态方法的void方法mock,模拟抛出异常

    public void setUp(){
        PowerMockito.mockStatic(ProfilerUtil.class);
        doThrow(new RuntimeException()).when(ProfilerUtil.class);
        ProfilerUtil.start("exception test");
    }

5.5、模拟测试构造函数

public class User{
	private String username;
	private String password;
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    public void insert(){
    	throw new UnsupportedOperationException();
    }	
}
public class UserService {
    public void saveUser(String username, String password) {
        User user = new User(username, password);
        user.insert();
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceTest {
    @Mock
    private User user;

    @Test
    public void saveUser() throws Exception {
        String username = "user1";
        String password = "aaa";
        // 在构造函数被调用的使用返回了我们构造的类
        PowerMockito.whenNew(User.class).withArguments(username, password).thenReturn(user);
        PowerMockito.doNothing().when(user).insert();
        UserService userService = new UserService();
        userService.saveUser(username, password);
        Mockito.verify(user).insert();
    }
}

5.6、如何测试是否打印了日志

public class Dummy{
	  private static LogCaptor logCaptor;
    private static final String EXPECTED_INFO_MESSAGE = "Keyboard not responding. Pre";

    @BeforeAll
    public static void setupLogCaptor() {
        logCaptor = LogCaptor.forClass(FooService.class);
    }

    @AfterEach
    public void clearLogs() {
        logCaptor.clearLogs();
    }

    @AfterAll
    public static void tearDown() {
        logCaptor.close();
    }

    @Test
    public void logMethod() {
        // do something that triggers logging in FooService
        // Assuming there is a method in FooService that logs the expected message.
        assertThat(logCaptor.getInfoLogs()).containsExactly(EXPECTED_INFO_MESSAGE);
    }
}

5.7、测试预期抛出异常

@Test(expected = IllegalStateException.class)
public void dummy() {
    // do something
    throw new IllegalStateException();
}

5.8、注入依赖


/**
 * 这个测试类展示了如何使用 PowerMock 和 Mockito 进行测试。
 * @RunWith(PowerMockRunner.class) 告诉 JUnit 使用 PowerMockRunner 进行测试。
 * @PrepareForTest({MockUtil.class}) 表示要为指定的类准备测试环境,这里是 MockUtil 类,
 * 适用于模拟 final 类或有 final、private、static、native 方法的类。
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({MockUtil.class})
public class MockExample{
    /**
     * @InjectMocks 注解用于将被测试类的依赖自动注入到该实例中。
     * 这里会将模拟的依赖注入到 MockServiceImpl 实例中。
     */
    @InjectMocks
    private MockServiceImpl mockService;
    /**
     * @Mock 注解用于创建模拟对象。这里创建了一个 MockMapper 的模拟对象。
     */
    @Mock
    private MockMapper mockMapper;

    /**
     * 测试方法,用于测试某个特定的功能。
     * 在这个方法中,首先创建了一个 MockModel 对象,然后使用 PowerMockito 模拟了 mockMapper 的 count 方法的返回值为 2。
     * 最后,使用 assertEquals 断言来验证 mockService 的 count 方法的返回值是否与预期一致。
     */
    @Test
    public void testSomething(){
        MockModel model = new MockModel();
        PowerMockito.when(mockMapper.count(model)).thenReturn(2);
        assertEquals(2, mockService.count(model));
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2234452.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kubernetes——part8-2 k8s集群存储解决方案 GlusterFS

一、存储解决方案介绍 1.1 GlusterFS GlusterFS是一个开源的分布式文件系统具有强大的横向扩展能力通过扩展能够支持数PB存储容量和处理数千客户端GlusterFS借助TCP/IP或InfiniBandRDMA网络将物理分布的存储资源聚集在一起&#xff0c;使用单一全局命名空间来管理数据。 1.2…

Kafka 之顺序消息

前言&#xff1a; 在分布式消息系统中&#xff0c;消息的顺序性是一个重要的问题&#xff0c;也是一个常见的业务场景&#xff0c;那 Kafka 作为一个高性能的分布式消息中间件&#xff0c;又是如何实现顺序消息的呢&#xff1f;本篇我们将对 Kafka 的顺序消息展开讨论。 Kafk…

js WebAPI黑马笔记(万字速通)

此笔记来自于黑马程序员&#xff0c;pink老师yyds 复习&#xff1a; splice() 方法用于添加或删除数组中的元素。 注意&#xff1a; 这种方法会改变原始数组。 删除数组&#xff1a; splice(起始位置&#xff0c; 删除的个数) 比如&#xff1a;1 let arr [red, green, b…

C 学习(5)

哈哈哈哈哈&#xff0c;终于想起来了&#xff01;贴一下主要的参考&#xff1a; 基本语法 - 《阮一峰《C 语言教程》》 - 书栈网 BookStack 内容写的比较浅显有疏漏&#xff0c;如果看不明白&#xff0c;再结合一下百度。 注释 C 语言的注释有两种表示方法。 第一种方法是…

redis7学习笔记

文章目录 1. 简介1.1 功能介绍1.1.1 分布式缓存1.1.2 内存存储和持久化(RDBAOF)1.1.3 高可用架构搭配1.1.4 缓存穿透、击穿、雪崩1.1.5 分布式锁1.1.6 队列 1.2 数据类型StringListHashSetZSetGEOHyperLogLogBitmapBitfieldStream 2. 命令2.1 通用命令copydeldumpexistsexpire …

【设计模式系列】建造者模式(十)

目录 一、什么是建造者模式 二、建造者模式的角色 三、建造者模式的典型应用 四、建造者模式在StringBuilder中的应用 五、典型建造者模式的案例 一、什么是建造者模式 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;用于构建复杂对…

用vite创建项目

一. vite vue2 1. 全局安装 create-vite npm install -g create-vite 2. 创建项目 进入你想要创建项目的文件夹下 打开 CMD 用 JavaScript create-vite my-vue2-project --template vue 若用 TypeScript 则 create-vite my-vue2-project --template vue-ts 这里的 …

JVM结构图

JVM&#xff08;Java虚拟机&#xff09;是Java编程语言的核心组件之一&#xff0c;负责将Java字节码翻译成机器码并执行。JVM由多个子系统组成&#xff0c;包括类加载子系统、运行时数据区、执行引擎、Java本地接口和本地方法库。 类加载子系统&#xff08;Class Loading Subsy…

WordPress伪静态设置

为什么要设置WordPress伪静态&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;中&#xff0c;静态URL通常被认为更易于搜索引擎爬虫抓取和索引&#xff0c;有助于提高网站的搜索引擎排名。 WordPress伪静态设置方法主要依赖于服务器环境&#xff0c;以下是针对不同服务器…

【黑盒测试】等价类划分法及实例

本文主要介绍黑盒测试之等价类划分法&#xff0c;如什么是等价类划分法&#xff0c;以及如何划分&#xff0c;设计等价类表。以及关于三角形案例的等价类划分法。 文章目录 一、什么是等价类划分法 二、划分等价类和列出等价类表 三、确定等价类的原则 四、建立等价类表 …

宠物领养救助管理软件有哪些功能 佳易王宠物领养救助管理系统使用操作教程

一、概述 佳易王宠物领养救助管理系统V16.0&#xff0c;集宠物信息登记、查询&#xff0c;宠物领养登记、查询&#xff0c; 宠物领养预约管理、货品进出库库存管理于一体的综合管理系统软件。 概述&#xff1a; 佳易王宠物领养救助管理系统V16.0&#xff0c;集宠物信息登记…

RK3568开发板静态IP地址配置

1. 连接SSH MYD-LR3568 开发板设置了静态 eth0:1 192.168.0.10 和 eth1:1 192.168.1.10&#xff0c;在没有串口时调试开发板&#xff0c;可以用工具 SSH 登陆到开发板。 首先需要用一根网线直连电脑和开发板&#xff0c;或者通过路由器连接到开发板&#xff0c;将电脑 IP 手动设…

Flutter错误: uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared

前言 今天要做蓝牙通信的功能&#xff0c;我使用了flutter_reactive_ble这个库&#xff0c;但是在运行的时候发现一下错误 Launching lib/main.dart on AQM AL10 in debug mode... /Users/macbook/Desktop/test/flutter/my_app/android/app/src/debug/AndroidManifest.xml Err…

【含开题报告+文档+源码】基于Java的房屋租赁服务系统设计与实现

开题报告 随着城市化进程的加速和人口流动性的增加&#xff0c;租房需求不断增长。传统的租赁方式往往存在信息不对称、流程不规范等问题&#xff0c;使得租户和房东的租赁体验不佳。而而房屋租赁系统能够提供便捷、高效的租赁服务&#xff0c;满足租户和房东的需求。房屋租赁…

斯托克斯矢量,表示电磁波的(不是散射体)平均后,可分解为完全极化电磁波和噪声

可见完全极化分就表示只有一种&#xff0c;在T矩阵中是只有一种散射体&#xff0c;在电磁波协方差矩阵中是只有一种电磁波

微服务day03

导入黑马商城项目 创建Mysql服务 由于已有相关项目则要关闭DockerComponent中的已开启的项目 [rootserver02 ~]# docker compose down WARN[0000] /root/docker-compose.yml: version is obsolete [] Running 4/4✔ Container nginx Removed …

大腾智能荣获盐田区黄金珠宝产业“产业赋能数字化优选能力伙伴”荣誉

11月2日&#xff0c;盐田区黄金珠宝产业数智化转型促进中心&#xff08;简称“促进中心”&#xff09;揭牌仪式圆满举办。盐田区委书记李忠&#xff0c;市工业和信息化局、市市场监督管理局、华为技术有限公司等相关单位、企业负责人共同见证促进中心揭牌启动。 大腾智能也出席…

10天进阶webpack---(2)webpack模块兼容性处理

回顾CMJ和ESM的区别 CMJ的本质可以使用一个函数概括 // require函数的伪代码 function require(path){if(该模块有缓存吗){return 缓存结果;}function _run(exports, require, module, __filename, __dirname){// 模块代码会放到这里}var module {exports: {}}_run.call(mod…

034_Structural_Transient_In_Matlab结构动力学问题求解

结构动态问题 问题描述 我们试着给前面已经做过的问题上加一点有趣的东西。 结构静力学求解 当时求解这个问题&#xff0c;在最外面的竖直切面加载了一个静态的固定的力。下面我们试试看在上方的表面增加一个脉冲压力载荷。 采用统一的有限元框架&#xff0c;定义问题&…

简单的 docker 部署ELK

简单的 docker 部署ELK 这是我的运维同事部署ELK的文档&#xff0c;我这里记录转载一下 服务规划 架构: Filebeat->kafka->logstash->ES kafka集群部署参照: kafka集群部署 部署服务程序路径/数据目录端口配置文件elasticsearch/data/elasticsearch9200/data/elas…