在实习过程中,首先要熟悉所在组所做的项目,而单元测试就是一个比较好的方法。
# 什么是单元测试
单元测试( Unit Testing
)是软件开发中的一种测试方法,旨在验证代码中最小可测试单元(通常是函数、方法或类)的正确性。它的主要目的是确保每个单元在各种情况下都能按预期工作,从而及早发现和纠正错误。
# 单元测试的特点
独立性:单元测试通常是独立的,不依赖于其他部分的代码。
自动化:单元测试可以通过自动化工具运行,减少人为错误。
快速反馈:由于测试范围小,单元测试能够快速提供反馈,帮助开发者及时修复问题。
# 单元测试的优点
提高代码质量:通过早期发现和修复错误,单元测试可以显著提高代码质量。
简化调试:由于测试范围小,定位和修复错误变得更加容易。
文档作用:单元测试代码可以作为文档,帮助其他开发者理解代码的功能和使用方法。
# 如何进行单元测试
# 使用 squareTest 插件
安装插件
找到要生成的类,点击
generate Test
更换给定的模板为以下文件中内容
补充必备代码
Public Class xxx extends NecpBaseContextTest
首先继承这个类添加以下静态代码块:
static { (new SqlResource()).handleEvent(Database.MYSQL, null); (new SqlResource()).handleEvent(Database.ORACLE, null); }
注意事项
tearDown()
方法相当于回收垃圾的作用,其中reset
方法参数只放被@Mock
注解的对象,@InjectMock
不需要,如果整个测试类没有@Mock
注解的对象,那么就不需要编写tearDown()
方法。例如:@Mock
private ArchivesCsRepository mockArchivesCsRepository;
@After
public void tearDown() {
reset(mockArchivesCsRepository);
}
# Mockito 框架用法要点
# 参数匹配和打桩
前言:
当你在测试中设置 mock
对象的行为(即打桩,使用 when(...).thenReturn(...)
等语句),你指定了当特定方法被特定参数调用时应该返回什么。
如果在实际执行测试时传递的参数与你设置的参数不匹配,那么打桩的行为将不会发生, mock
对象将返回默认值(如 null
、 0
或 false
等)。
例如:
--- 被测试方法片段 ---
String sql = ARCH_QUERY_DOCNO_SQL; | |
sql = MessageFormat.format(sql, tableName); | |
Map<String, Object> param = new HashMap<>(); | |
List<ArchMoveBO> result = moveQueryService.findListByParam(sql, param , ArchMoveBO.class); | |
if (null == result || result.isEmpty()) { | |
return "T000001"; | |
} |
---- 错误的测试方法片段 ----
final List<ArchMoveBO> archMoveBOS = Arrays.asList(archMoveBO); | |
when(queryService.findListByParam("sql",newHashMap<>(), archMoveBO.class)).thenReturn(archMoveBOS); | |
final String result = archivesMoveQueryServiceUnderTest.getNextProcessInstId("year"); |
执行结果: 被测方法返回的 result 为 null,这就说明测试方法打桩失效了。
---- 正确的测试方法片段 ----
final List<ArchMoveBO> archMoveBOS = Arrays.asList(archMoveBO); | |
when(queryService.findListByParam(any(),anyMap(), any())).thenReturn(archMoveBOS); | |
final String result = archivesMoveQueryServiceUnderTest.getNextProcessInstId("year"); |
执行结果: 被测方法返回的 result
为 archMoveBOS
,这就说明测试方法打桩成功了。
结论: 当使用 mock
来 mock
方法的时候,一定要注意参数匹配, 否则打桩不成功。
补充: 在使用参数任意匹配符时,也要注意匹配,有的时候 Any()
可以成功,但是有的时候需要具体匹配,比如使用 anyString()
;
# 如何测试 private 方法?
(1) 使用这个方法 Whitebox.invokeMethod()
调用私有方法即可。
例如:
boolean[] result = Whitebox.invokeMethod(archivesPrintQueryService, "isPrintEleOrOther", nodeTypeCodeMap, isVoucher); |
注意点:
(1) 使用 Whitebox
调用私有方法时不能 debug
调试。
(2) 一定要注意方法的返回值,如果返回值不对应就会报错,但是这个错是运行时的错误,不会在编译的时候爆出来,一旦出错难以找出,一定要小心。
在 Java
中, Whitebox
类通常来自于 org.jmockit.whitebox
包,它是 Mockit
框架的一部分,用于测试时访问私有方法。 Whitebox.invokeMethod()
方法用于调用一个类的私有方法,即使这些方法不是公开的,也可以通过反射机制来访问它们。
下面是 Whitebox.invokeMethod()
方法的签名:
public static <T> T invokeMethod(Object obj, String methodName, Class<?>[] parameterTypes, Object[] parameters) |
archivesPrintQueryService
: 这是你要调用私有方法的对象实例。在这个例子中,它是一个实现了 archivesPrintQueryService
接口或类的实例。
" isPrintEleOrOther
": 这是你要调用的私有方法的名称。在这个字符串中,方法名是 isPrintEleOrOther
,注意,方法名不需要加任何修饰符,即使是私有方法。nodeTypeCodeMap
: 这是第一个参数传递给 isPrintEleOrOther
方法的值。 nodeTypeCodeMap
通常是一个映射(如 HashMap
或 Map
),它包含了节点类型代码和其它相关数据的键值对。isVoucher
: 这是第二个参数传递给 isPrintEleOrOther
方法的值。这个参数通常是布尔值,表示是否是凭证( isVoucher
为 true
表示是凭证,为 false 表示不是)。
下面是如何使用 Whitebox.invokeMethod()
调用的详细步骤:
archivesPrintQueryService
对象是一个包含私有方法isPrintEleOrOther
的类的实例。Whitebox.invokeMethod()
被用来调用这个实例的私有方法isPrintEleOrOther
。nodeTypeCodeMap
和isVoucher
作为参数传递给这个私有方法。- 私有方法
isPrintEleOrOther
执行其逻辑,可能基于传入的参数。 - 执行结果是一个
boolean[]
数组,这个数组被赋值给result
变量。 - 总结一下,
Whitebox.invokeMethod()
调用了archivesPrintQueryService
对象的私有方法isPrintEleOrOther
,并传递了两个参数nodeTypeCodeMap
和isVoucher
,然后返回了一个boolean[]
类型的值,这个值被存储在result
变量中。
# 如何测试 static 方法
(1) 添加 @PrepareForTest(Class.class)
注解
(2) 模拟 static
方法: PowerMockito.mockStatic(ClassWithstaticMethod.class)
(3) 定义 static
方法的行为: when().thenReturn()
;
注意点:如果出现问题,比如 not prepared for test.
可能是 PowerMockito
和 Mockito
版本兼容性问题。
# 如何 Mock 被测试类的方法中调用的方法
无需 mock
,直接陷入调用的方法即可。
# 如何设置待测方法的属性值
例如: @Value("${formatFileTagLimit:10000}")
private Integer formatFileTagLimit;
解决: 我们可以通过反射来进行属性注入。
@Before | |
public void setUp() throws Exception { | |
ReflectionTestUtils.setField(archFormatFileTagServiceUnderTest, "formatFileTagLimit", 100); | |
} |
# this.getDao () 为 NULL 的解决办法
以 ArchivesDepotQueryServiceTest
类为例子:
@InjectMocks | |
private ArchivesDepotQueryService service; | |
@Mock | |
private ArchivesDepotRepository repository; | |
@Before | |
public void setUp() throws Exception { | |
MockitoAnnotations.initMocks(this); | |
Field daoField = GeneralContext.class.getDeclaredField("dao"); | |
daoField.setAccessible(true); | |
daoField.set(service, repository); | |
} |
解释(通义灵码生成):
这段标红代码的作用是通过反射机制,修改 GeneralContext
类中 dao
字段的值为 repository
。具体步骤如下:
(1) 使用 getDeclaredField
方法获取 GeneralContext
类中名为 dao
的私有字段;
(2) 调用 setAccessible
方法将该字段的访问权限设置为可访问;
(3) 使用 set
方法将 dao
字段的值设置为 repository
,并将 service
对象作为参数传入,表示要修改的是 service
对象中的 dao
字段值。
# 如何解决微服务调用的问题
以 ArchivesAttachServiceTest
类为例:
步骤:
(1) 加入 @PrepareForTest({MappService.class})
(2) 对 MapperService
进行 mock
注入
@Mock | |
MappService mappService; |
(3) 在 setUp()
方法中添加如下代码:
@Before | |
public void setUp() throws Exception { | |
Whitebox.setInternalState(MappService.class, "INSTANCE", mappService); | |
when(MappService.INSTANCE.doPost(anyString(),anyMap(),eq(String.class), anyObject())).thenReturn(""); | |
} |
缺陷:
虽然 mock
成功,不会报错,但是不能设定返回值, eturn
为”” 空的。
# 解决 DBUtil.isOracle () 报错的方式
(1) 在 @PrepareForTest(DBUtil.class)
中添加 DBUtil.class
(2)在相应的方法中添加这两句即可
PowerMockito.mockStatic(DBUtil.class); | |
PowerMockito.when(DBUtil.isOracle()).thenReturn( true); |
# 解决静态常量为空的情况
例如:
PowerMockito.mockStatic(SQLConst.class); | |
Field field = PowerMockito.field(SQLConst.class, "QYVO_STA_MITEM_SY_MCOM_SITEM"); | |
field.set(null, "your_sql_query_here"); |
# 让某个方法什么也不做,从而跳过该方法
doNothing().when(spy).saveList(anyList()); |
# 已知会抛出某个异常.
import org.junit.jupiter.api.Test; | |
import static org.junit.jupiter.api.Assertions.assertThrows; | |
Exception exception = assertThrows(RuntimeException.class, () -> { | |
// 在这里调用可能抛出 NullPointerException 的方法 | |
CommentSaveStatusEnum.valueOf(2); | |
}); |
# 使用 when 来抛出异常
when(mockSystemParamContext.findByNameAndTenantGid(anyString(), anyString())).thenThrow(new RuntimeException("Error fetching parameter")); |