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

如何正确写单元测试

  • 单元测试重要性
  • 写单元测试时存在的问题
  • 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{@Testpublic 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 {@ResourceBuyerPlanMapper buyerPlanMapper;@Testpublic 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;@Beforepublic 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 {@Mockprivate User user;@Testpublic 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";@BeforeAllpublic static void setupLogCaptor() {logCaptor = LogCaptor.forClass(FooService.class);}@AfterEachpublic void clearLogs() {logCaptor.clearLogs();}@AfterAllpublic static void tearDown() {logCaptor.close();}@Testpublic 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 somethingthrow 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 实例中。*/@InjectMocksprivate MockServiceImpl mockService;/*** @Mock 注解用于创建模拟对象。这里创建了一个 MockMapper 的模拟对象。*/@Mockprivate MockMapper mockMapper;/*** 测试方法,用于测试某个特定的功能。* 在这个方法中,首先创建了一个 MockModel 对象,然后使用 PowerMockito 模拟了 mockMapper 的 count 方法的返回值为 2。* 最后,使用 assertEquals 断言来验证 mockService 的 count 方法的返回值是否与预期一致。*/@Testpublic void testSomething(){MockModel model = new MockModel();PowerMockito.when(mockMapper.count(model)).thenReturn(2);assertEquals(2, mockService.count(model));}
}

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

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

相关文章

算法: 链表题目练习

文章目录 链表题目练习两数相加两两交换链表中的节点重排链表合并 K 个升序链表K 个一组翻转链表 总结 链表题目练习 两数相加 坑: 两个链表都遍历完后,可能需要进位. class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {ListNode cur1 l1;ListNode…

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

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

【Pikachu靶场:XSS系列】xss之过滤,xss之htmlspecialchars,xss之herf输出,xss之js输出通关啦

一、xss之过滤 <svg onloadalert("过关啦")> 二、xss之htmlspecialchars javascript:alert(123) 原理&#xff1a;输入测试文本为herf的属性值和内容值&#xff0c;所以转换思路直接变为js代码OK了 三、xss之href输出 JavaScript:alert(假客套) 原理&#x…

JS装备智能化储备管理体系优化改革

现代化的JS仓储管理方案&#xff0c;通过整合先进的RFID技术与三维模拟技术&#xff0c;为JS物流领域开创了新颖的改革浪潮。以下是对这两项尖端技术融合并用于战备物资管理的应用概述&#xff1a; 一、RFID技术在JS物资管理中的实践 RFID技术依靠无线电波实现无需直接接触的数…

缓存淘汰策略:Redis中的内存管理艺术

在现代应用架构中&#xff0c;缓存是提升性能的关键组件。 Redis&#xff0c;作为一个高性能的键值存储系统&#xff0c;因其快速的数据访问能力而被广泛使用。然而&#xff0c;由于物理内存的限制&#xff0c;Redis必须在存储空间和性能之间找到平衡&#xff0c;这就引出了缓…

AUTOSAR COM 与 LargeDataCOM 模块解析及 C++ 实现示例

AUTOSAR COM 和 LargeDataCOM 模块在功能和使用场景上有一些显著的区别。以下是它们的主要区别及具体的应用示例,最后用 C++ 源代码来解析说明。 AUTOSAR COM 模块 • 功能:主要用于处理标准大小的信号和 I-PDU(协议数据单元),提供了信号打包、解包、数据传输和接收等功能…

JavaWeb复习

在网络应用程序中有两种基本的结构&#xff0c;即C/S和B/S&#xff0c;对于c/s程序分为客户机和服务器两层&#xff0c;把应用软件按照在客户机端(通常由客户端维护困难)&#xff0c;通过网络与服务器进行相互通信。B/S结构却不用通知客户端安装某个软件&#xff0c;内容修改了…

qt获取本机IP和定位

前言&#xff1a; 在写一个天气预报模块时&#xff0c;需要一个定位功能&#xff0c;在网上翻来翻去才找着&#xff0c;放在这里留着回顾下&#xff0c;也帮下有需要的人 正文&#xff1a; 一开始我想着直接调用百度地图的API来定位&#xff0c; 然后我就想先获取本机IP的方…

python爬取旅游攻略(1)

参考网址&#xff1a; https://blog.csdn.net/m0_61981943/article/details/131262987 导入相关库&#xff0c;用get请求方式请求网页方式&#xff1a; import requests import parsel import csv import time import random url fhttps://travel.qunar.com/travelbook/list.…

Oracle OCP认证考试考点详解082系列12

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 56. 第56题&#xff1a; 题目 解析及答案&#xff1a; 关于企业管理器&#xff08;EM&#xff09;Express&#xff0c;以下哪两个陈述是…

Postgresql源码(137)执行器参数传递与使用

参考 《Postgresql源码&#xff08;127&#xff09;投影ExecProject的表达式执行分析》 0 总结速查 prepare p_04(int,int) as select b from tbl_01 where a $1 and b $2为例。 custom计划中&#xff0c;在表达式计算中使用参数的值&#xff0c;因为custom计划会带参数值&…

SPI通信详解-学习笔记

参考原文地址 SPI&#xff1a;高速、全双工&#xff0c;同步、通信总线 SPI主从模式 SPI分为主、从两种模式&#xff0c;一个SPI通讯系统需要包含一个&#xff08;且只能是一个&#xff09;主设备&#xff0c;一个或多个从设备。提供时钟的为主设备&#xff08;Master&#xff…

Day102漏洞发现-漏扫项目篇Poc开发Yaml语法插件一键生成匹配结果交互提取

知识点&#xff1a; 1、Nuclei-Poc开发-环境配置&编写流程 2、Nuclei-Poc开发-Yaml语法&匹配提取 3、Nuclei-Poc开发-BurpSuite一键生成插件 Nuclei-Poc开发-环境配置&编写流程 1、开发环境&#xff1a;VscodeYaml插件 Visual Studio Code - Code Editing. R…

Node.js 入门指南:从零开始构建全栈应用

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;node.js篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来node.js篇专栏内容:node.js-入门指南&#xff1a;从零开始构建全栈应用 前言 大家好&#xff0c;我是青山。作…

WordPress网站添加嵌入B站视频,自适应屏幕大小,取消自动播放

结合bv号 改成以下嵌入式代码&#xff08;自适应屏幕大小,取消自动播放&#xff09; <iframe style"width: 100%; aspect-ratio: 16/9;" src"//player.bilibili.com/player.html?isOutsidetrue&bvidBV13CSVYREpr&p1&autoplay0" scrolling…

大模型应用系列:Query 变换的示例浅析

【引】NLP中的经典组件在大模型应用中还有效么&#xff1f;大模型对自然语言处理中的典型任务有什么影响么&#xff1f; RAG应用通过分割文档、嵌入向量化并检索高语义相似性的块来响应用户问题&#xff0c;但面临文档块不相关、用户用词不当及结构化查询需求等问题。若RAG无法…

【Oracle】空格单字符通配符查询匹配失败

问题 在进行模糊查询的时候&#xff0c;通过全局任意字符串匹配出含有两个字刘姓的人&#xff0c;但是通过刘_不能匹配出结果。 解决 检查后发现&#xff0c;姓名中包含空格 SELECT * FROM student WHERE TRIM(sname) LIKE 刘_;第一种解决方案就是查询的时候进行去空格处理&a…

讲讲⾼并发的原则?

大家好&#xff0c;我是锋哥。今天分享关于【讲讲⾼并发的原则&#xff1f;】面试题。希望对大家有帮助&#xff1b; 讲讲⾼并发的原则&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 高并发是指系统在同一时间内能够处理大量请求的能力。要有效地管理…

鸿蒙进阶-AlphabetIndexer组件

大家好&#xff0c;这里是鸿蒙开天组&#xff0c;今天我们来学习AlphabetIndexer组件&#xff0c;喜欢就点点关注吧&#xff01; 通过 AlphabetIndexer 组件可以与容器组件结合&#xff0c;实现导航联动&#xff0c;以及快速定位的效果 核心用法 AlphabetIndexer不是容器组件…

ubuntu交叉编译expat库给arm平台使用

1.下载expat库源码: https://github.com/libexpat/libexpat/release?page=2 wget https://github.com/libexpat/libexpat/release/download/R_2_3_0/expat-2.3.0.tar.bz2 下载成功: 2.解压expat库,并进入解压后的目录: tar xjf expat-2.3.0.tar.bz2 cd expat-2.3.0 <…