Java中的单元测试
1、概念介绍
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。单元是人为规定的最小的被测功能模块。
本文主要讲Java中的单元测试中的代码编写,一般最小的单元就是一个方法,设计测试场景(一些边界条件),看运行结果是否满足预期,修改了代码也能帮助验证是否影响了原有的逻辑。
2、常用的Java测试框架
Junit:是一个为Java编程语言设计的单元测试框架。
Mockito:允许程序员使用自动化的单元测试创建和测试模拟对象。
PowerMock:PowerMock利用自定义的类加载器和字节码操纵器,来确保静态方法的模拟、静态初始化的删除、函数构造、最终的类和方法以及私有方法。
实际上还有许多优秀的测试框架,这几种笔者比较常用,所以本文记录下使用方法。
3、Maven引入
相关的Maven依赖如下
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.9.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency>
彼此间有版本对应关系,没对应可能会出现冲突,其官网的版本对应关系如下:
github的Wiki地址:https://github.com/powermock/powermock/wiki/Mockito
4、自动生成单元测试代码
- Idea引入插件
- 选择要生成单元测试的类,按Alt+Insert出现如下界面,选择TestMe自动生成文件
- 选择需要的生成模板,可以根据自己实际引入的依赖选择,此处选择Junit4+Mockito
- 生成的代码如下,可以生成一些基本的方法和注解,然后根据实际情况修改,可以节省一部分工作量。
5、常用注解和配置
@Mock:创建一个模拟的对象,类似于@Autowired,但不是真实的对象,是Mock对象,这个注解使用在类属性上
@InjectMocks:创建一个实例,其余用@Mock注解创建的mock将被注入到用该实例中,这个注解使用在类属性上
@RunWith:表示一个运行器,@RunWith(PowerMockRunner.class)表示指定用PowerMockRunner运行,这个注解使用在类上
@PowerMockIgnore:这个注解表示将某些类延迟到系统类加载器加载,解决一些类加载异常。(具体类加载异常实际中还未遇见,后续补充),这个注解在类和方法上使用
@PrepareForTest:这个注解告诉PowerMock为测试准备某些类,通常是那些需要字节码操作的类。这包括带有final、private、static或native方法的类,new一个对象时,需要特殊处理(见下面的whenNew),这个注解在类和方法上使用
@Test:@Test修饰的public void方法可以作为测试用例运行。Junit会构造一个新的类实例,然后调用所有加了@Test的方法,方法执行过程中的任何异常,都会被判定为测试用例执行失败。
@Before:@Before注解的方法中的代码会在@Test注解的方法中首先被执行。可以做一些公共的前置操作:加入一些申请资源的代码:申请数据库资源,申请io资源,申请网络资源,new一些公共的对象等等。
@After:@After注解的方法中的代码会在@Test注解的方法中首先被执行。可以做一些公共的后置操作,如关闭资源的操作。
注:可以查看注解上的注释,了解其大致用法。
6、常规用法
下面给一个使用上述所有注解的简单例子:
import org.example.dto.CallbackDTO; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import java.io.File; import java.util.Map; @RunWith(PowerMockRunner.class) @PowerMockIgnore({"org.xml.*", "javax.xml.*"}) @PrepareForTest({File.class, MessageCardService.class}) public class MessageCardServiceTest { @InjectMocks MessageCardService messageCardService; @Mock SendService sendService; @Before public void setUp() { System.out.println("前置操作"); } @After public void after() { System.out.println("后置操作"); } @Test public void sendMessage() { PowerMockito.when(sendService.getMsg()).thenReturn("test"); Map map = messageCardService.updateMessageCard(new CallbackDTO()); Assert.assertEquals(map.get("key"), null); } }
运行结果如下:
被单元测试的方法源码如下:
@Service public class MessageCardService { @Autowired private SendService sendService; public String testReturnValue(String param) { //调用其他类的方法 param = param + "-" + "newValue"; String msg = sendService.getMsg(param); return msg; } public String testTimes(String param, Integer times) { //调用其他类的方法 String msg = ""; for (int i = 0; i < times; i++) { msg = sendService.getMsg(param); } return msg; } public void testNewObject() { //new一个文件和日期 File file = new File("path"); Date now = new Date(); System.out.println("date:" + now + ",length:" + file.length()); } public void testStatic(String param) { //调其他类的静态方法 String s = SendService.sendMsg(param); System.out.println("static result:" + s); } public void testPrivate(String param) { //调用自身的私有方法 String msgHello = msgHello(param); System.out.println("msgHello:" + msgHello); } public void testVoid(String param) { //调用其他类的无返回值的方法 sendService.printMsg(param); } private String msgHello(String param) { return param; } }
@Service public class SendService { public String getMsg(String msg) { return msg; } public void printMsg(String param) { System.out.println(param); } public static String sendMsg(String param) { return param; } }
6.1、when().thenReturn()
当调用mock对象的方法时,根据入参条件,返回指定的值。只调用一次thenReturn(),则调用mock方式时始终返回相同的值;如果多次thenReturn(),可以在调用同一方法时,根据调用次数返回不同的值,如下:
/** * Mock普通方法的返回值 * 使用场景:当方法入参为某一值或者任何值时,返回一个模拟的结果 */ @Test public void testWhen() { //mock方法sendService.getMsg()的返回值,当入参为test时,第一次返回值为newValue,第二次返回new2Value PowerMockito.when(sendService.getMsg("test")).thenReturn("newValue").thenReturn("new2Value"); //运行需要测试的实际方法 String result = messageCardService.testReturnValue("test"); //比较预期结果 Assert.assertEquals("test-newValue", result); }
6.2、whenNew().thenReturn()和verifyNew()
当需要new一个对象时,可以根据条件返回一个mock对象,下面的示例,当new File()时,如果入参是path,则返回mock的文件对象。注意:需要new 一个mock对象时,需要将 被测试类(@InjectMocks修饰的类)放入到@PrepareForTest注解中。
示例代码如下:
/** * Mock new一个对象,并验证实例化次数是否满足预期 * 使用场景: * 1、有时候,被测试方法里面需要new一些对象,为了让对象可控就可以采用whenNew的方式 * 2、验证new的对象个数是否符合预期,可以采用verifyNew */ @Test public void testWhenNewAndVerifyNew() throws Exception { //Mock new的File对象 File mockFile = PowerMockito.mock(File.class); PowerMockito.when(mockFile.exists()).thenReturn(true); PowerMockito.when(mockFile.isFile()).thenReturn(true); PowerMockito.when(mockFile.length()).thenReturn(100L); PowerMockito.whenNew(File.class).withArguments("path").thenReturn(mockFile); //mock new的Date对象,可以定义一个类变量或者实际的对象,从而得到一个固定的结果 Date mockDate = new Date(); PowerMockito.whenNew(Date.class).withNoArguments().thenReturn(mockDate); //运行需要测试的实际方法 messageCardService.testNewObject(); //验证是否mock new了一个日期实例(注意这里只能验证预期的对象创建,就是whenNew的对象次数,不是验证真实的对象new了几次) PowerMockito.verifyNew(Date.class, Mockito.times(1)).withNoArguments(); //验证是否mock new了一个文件实例 PowerMockito.verifyNew(File.class, Mockito.times(1)).withArguments("path"); }
注意:verifyNew()只能验证预期的对象创建,就是whenNew的对象次数,不是真实的对象创建次数,即如果该对象没有被mock,而是真实的创建对象,则次数不会被统计。
verifyNew()时需要将@InjectMocks修饰的类放入@PrepareForTest注解中,如下图:
6.3、Mockito.verify()
- 验证中间过程的方法入参是否符合预期,例如中间某个方法的入参经过了计算得到,则可以验证此入参是否符合预期
/** * 验证中间过程的方法入参是否符合预期 * 使用场景:被测试方法中有一个方法的入参是经过一系列运算得到,可以用于验证入参的运算逻辑是否符合预期 */ @Test public void testVerifyParam() { //mock方法sendService.getMsg()的返回值,当入参为test时,返回值为newValue PowerMockito.when(sendService.getMsg("test")).thenReturn("newValue"); //运行需要测试的实际方法 messageCardService.testReturnValue("test"); //验证sendService.getMsg(string s)入参是否是test-newValue Mockito.verify(sendService).getMsg("test-newValue"); }
- 验证中间过程的方法被调用的次数是否符合预期
/** * 验证中间过程的方法被调用的次数 * 使用场景:当存在循环调用某个方法时,可以验证方法被调用的次数是否符合预期 */ @Test public void testVerifyTimes() { //mock方法sendService.getMsg()的返回值,当入参为test时,返回值为newValue PowerMockito.when(sendService.getMsg(Mockito.anyString())).thenReturn("newValue"); //运行需要测试的实际方法 messageCardService.testTimes("test", 2); //验证sendService.getMsg(string s)被调用次数是否为2次 Mockito.verify(sendService, Mockito.times(2)).getMsg(Mockito.anyString()); }
6.4、PowerMockito.doNothing().when()
mock无返回值的方法
/** * mock无返回值的方法(返回值为void) * 使用场景:比如有些操作是一个异步操作,无返回值,那么执行测试代码时,就可以采用doNothing */ @Test public void testReturnVoid() { //mock一个无返回值的普通方法 PowerMockito.doNothing().when(sendService).printMsg("test"); //运行需要测试的实际方法 messageCardService.testVoid("test"); }
6.5、PowerMockito.method()和Whitebox.invokeMethod()
mock私有方法,有两种方式,代码如下:
方式一:
/** * mock私有方法 */ @Test public void testPrivate() throws InvocationTargetException, IllegalAccessException { //@InjectMocks注入MessageCardService,指定私有方法 Method msgHello = PowerMockito.method(MessageCardService.class, "msgHello", String.class); //调用私有方法 Object result = msgHello.invoke(messageCardService, "hello"); //比较预期结果 Assert.assertEquals("hello", result); }
方式二:
/** * mock私有方法2 */ @Test public void testPrivate2() throws Exception { //Whitebox 调用私有方法 Object result = Whitebox.invokeMethod(messageCardService, "msgHello", "hello"); //比较预期结果 Assert.assertEquals("hello", result); }
6.6、PowerMockito.mockStatic()和verifyStatic()
mock静态方法,需要注意将调用的静态方法的类放入@PrepareForTest注解,如下图:
代码如下:
/** * mock静态方法 * 使用场景: * 1、mock静态方法使用mockStatic * 2、需要验证静态方法的调用次数,可以使用verifyStatic,后面紧跟需要验证的静态方法 */ @Test public void testStaticAndVerifyStatic() { //mock有返回值的静态方法 PowerMockito.mockStatic(SendService.class); PowerMockito.when(SendService.sendMsg("test")).thenReturn("newValue"); //运行需要测试的实际方法 messageCardService.testStatic("test"); //验证静态方法的执行次数 PowerMockito.verifyStatic(SendService.class, Mockito.times(1)); SendService.sendMsg("test"); }
6.7、PowerMockito.verifyPrivate()
验证私有方法调用次数,被测试方法调用自身的私有方法,有时候需要统计次数,则可以采用verifyPrivate()
注意:只有spy的对象才能验证私有方法的调用次数,mock的对象不行。doReturn和thenReturn的区别在于,doReturn不会进入到方法里面直接返回,thenReturn会先走方法再返回
/** * 验证被测试方法调用的私有方法次数 */ @Test public void testVerifyPrivate() throws Exception { //spy一个对象 MessageCardService spy = PowerMockito.spy(new MessageCardService()); //mock私有方法 PowerMockito.doReturn("hello").when(spy, "msgHello", "test"); //运行需要测试的实际方法 spy.testPrivate("test"); //验证 PowerMockito.verifyPrivate(spy, Mockito.times(1)).invoke("msgHello", "test"); }
7、总结
其实在正常的单元测试中,可能一个方法会很复杂,里面会同时包含很多种情形,这时候需要我们灵活组合,一些基本的操作在上面都能找到,能满足大部分需求。doAnswer用于修改指定方法的逻辑可以自定义,doThrow用来抛出异常,覆盖异常场景,还有一些其他的操作,这里暂不写,后续有空补上。
8、问题补充
8.1、Mock无返回值的静态方法
mock静态方法,需要注意将调用的静态方法的类放入@PrepareForTest注解,如下图:
方式一:
PowerMockito.mockStatic(SendService.class); PowerMockito.doNothing().when(SendService.class,"methodName",param1,param2);
方式二:
PowerMockito.mockStatic(SendService.class); PowerMockito.doNothing().when(SendService.class); SendService.dealMsg(msgDto,"test msg");
第二种方式,在方法调用时,入参如果用到any()匹配,可能会抛出 InvalidUseOfMatcherException,可以在方法入参上加参数匹配器,如下:
PowerMockito.mockStatic(SendService.class); PowerMockito.doNothing().when(SendService.class); SendService.dealMsg(ArgumentMatchers.eq(msgDto),ArgumentMatchers.eq("test msg"));
方式三(补充):
其实mock静态无返回值的方法,只需要将静态方法的类放入@PrepareForTest注解,然后mockStatic()对应的类即可,不需要显示的mock其方法,如下:
PowerMockito.mockStatic(SendService.class);
8.1、Mock被final修饰的类
同mock静态方法类似,需要注意将mock的类放入@PrepareForTest注解,如下图:
mock final修饰的类很简单,方法如下
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」