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修饰的类很简单,方法如下

posted @ 2023-03-07 20:50  浪迹天涯的派大星  阅读(6800)  评论(0编辑  收藏  举报