单元测试那些坑

1. 前言

为了提高白盒测试覆盖率,项目中需要添加单元测试代码,写单元测试中很多都是用的Mock+Junit,但是我这个项目中使用的是Mock+testng,不过这两种方式我都会介绍。

2. Mock+TestNG单元测试

2.1 前提准备

这里提供一份依赖jar包的pom文件:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>7.0.0</version>
    <scope>test</scope>
</dependency>
<!--静态类方法模拟-->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-core</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-testng</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
</dependency>

2.2 接口方法测试

在接口中我们肯定会有一些自动注入(@Autowired)的Dao层对象或者其他对象,在Mock中有两种方式表示这些自动注入的对象

2.2.1 使用注解自动初始化

自动化注解需要使用@InjectMocks@Mock搭配使用,这样就可以在测试类运行的时候Mock这些自动注入对象,之后在@BeforeTest 中使用MockitoAnnotations.initMocks(this); 就可以了。

实例代码:

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;

import com.xing.springDataJpa.first.FirstEntity;
import com.xing.springDataJpa.first.FirstRepository;
import com.xing.springDataJpa.service.impl.IndexServiceImpl;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.ResponseEntity;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

/**
 * 功能描述
 *
 * @since 2020-09-28
 */
public class IndexServiceImplTest {
    @Mock
    private FirstRepository firstRepository;

    // @InjectMocks: 创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
    @InjectMocks
    private IndexServiceImpl service;
    
    @BeforeTest
    public void beforeTest() {
        System.out.println("========= beforeTest() ==========");
        // 其中this就是单元测试所在的类,在initMocks函数中Mockito会根据类中不同的注解(如@Mock, @Spy等)创建不同的mock对象,即初始化工作
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void getPerson() {
        String userId = "3423423113";
        String userName = "xiaoming";
        FirstEntity firstEntity = new FirstEntity();
        firstEntity.setUserId(userId);
        firstEntity.setUserName(userName);
        when(firstRepository.findFirstByUserIdAndAndUserName(anyString(), anyString()))
                .thenReturn(firstEntity);
        ResponseEntity<Object> person = service.getPerson(userId, userName);
        Assert.assertNotNull(person);
    }
    
}

2.2.2 使用反射机制初始化注入对象

这种是很简便的使用,如果想多写几行代码可以使用反射机制,set这些对象。

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;

import com.xing.springDataJpa.first.FirstEntity;
import com.xing.springDataJpa.first.FirstRepository;
import com.xing.springDataJpa.service.impl.IndexServiceImpl;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.ResponseEntity;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import java.lang.reflect.Field;

/**
 * 功能描述
 *
 * @since 2020-09-28
 */
public class IndexServiceImplTest {
    IndexServiceImpl service = new IndexServiceImpl();
    
    @Mock
    private FirstRepository firstRepository;
    
    @BeforeTest
    public void beforeTest() throws NoSuchFieldException, IllegalAccessException {
        System.out.println("========= beforeTest() ==========");
        // 其中this就是单元测试所在的类,在initMocks函数中Mockito会根据类中不同的注解(如@Mock, @Spy等)创建不同的mock对象,即初始化工作
        MockitoAnnotations.initMocks(this);
        
        Class serviceClass = service.getClass();
        // 反射获取属性
        Field firstRepositoryField = serviceClass.getDeclaredField("firstRepository");
        firstRepositoryField.setAccessible(true);
        firstRepositoryField.set(service, firstRepository);
    }
    
    @Test
    public void getPerson() {
        String userId = "3423423113";
        String userName = "xiaoming";
        FirstEntity firstEntity = new FirstEntity();
        firstEntity.setUserId(userId);
        firstEntity.setUserName(userName);
        when(firstRepository.findFirstByUserIdAndAndUserName(anyString(), anyString()))
                .thenReturn(firstEntity);
        ResponseEntity<Object> person = service.getPerson(userId, userName);
        Assert.assertNotNull(person);
    }
    
}

2.3 静态类方法测试

在复杂的接口业务中,经常会用到一些工具类,静态方法等等,这类单元测试需要通过PowerMockito来实现静态方法的模拟。

静态方法:

public class StaticUtils {
    
    public static String printList(List<String> list) {
            System.out.println("============ StaticUtils.printHello()============");
            list.forEach(System.out::println);
            return "printList";
        }
}

接口类添加静态方法:

@Override
    public ResponseEntity<Object> getPerson(String userId, String userName) {
        String s = StaticUtils.printList(Arrays.asList("a", "b", "c"));
        FirstEntity firstEntity = firstRepository.findFirstByUserIdAndAndUserName(userId,userName);
        return ResponseEntity.ok().body(firstEntity);
    }

PowerMockito.mockStatic(StaticUtils.class); 要放在 MockitoAnnotations.initMocks(this); 前面才可以,示例代码:

import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;

import com.xing.springDataJpa.first.FirstEntity;
import com.xing.springDataJpa.first.FirstRepository;
import com.xing.springDataJpa.service.impl.IndexServiceImpl;
import com.xing.springDataJpa.utils.StaticUtils;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.testng.PowerMockObjectFactory;
import org.springframework.http.ResponseEntity;
import org.testng.Assert;
import org.testng.IObjectFactory;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.ObjectFactory;
import org.testng.annotations.Test;

/**
 * 功能描述
 *
 * @since 2020-09-28
 */
@PrepareForTest(StaticUtils.class)
public class IndexServiceImplTest  {
    @Mock
    private FirstRepository firstRepository;

    // @InjectMocks: 创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
    @InjectMocks
    private IndexServiceImpl service;
    
    @ObjectFactory
    public IObjectFactory getObjectFactory() {
        return new PowerMockObjectFactory();
    }
    
    @BeforeTest
    public void beforeTest() throws NoSuchFieldException, IllegalAccessException {
        System.out.println("========= beforeTest() ==========");
        // 其中this就是单元测试所在的类,在initMocks函数中Mockito会根据类中不同的注解(如@Mock, @Spy等)创建不同的mock对象,即初始化工作
        PowerMockito.mockStatic(StaticUtils.class);
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void getPerson() {
        String userId = "3423423113";
        String userName = "xiaoming";
        FirstEntity firstEntity = new FirstEntity();
        firstEntity.setUserId(userId);
        firstEntity.setUserName(userName);
        when(firstRepository.findFirstByUserIdAndAndUserName(anyString(), anyString()))
                .thenReturn(firstEntity);
        // 模拟静态方法
        String rel = "rel";
        PowerMockito.when(StaticUtils.printList(anyListOf(String.class)))
                .thenReturn(rel);
        ResponseEntity<Object> person = service.getPerson(userId, userName);
        Assert.assertNotNull(person);
    }
    
}

3. Mock+Junit单元测试

完整的代码:

import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;

import com.xing.springDataJpa.first.FirstEntity;
import com.xing.springDataJpa.first.FirstRepository;
import com.xing.springDataJpa.service.impl.IndexServiceImpl;
import com.xing.springDataJpa.utils.StaticUtils;

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.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.http.ResponseEntity;


/**
 * 功能描述
 *
 * @since 2020-09-28
 */

@RunWith(PowerMockRunner.class)
@PrepareForTest(StaticUtils.class)
public class IndexServiceImplTest  {
    @Mock
    private FirstRepository firstRepository;

    // @InjectMocks: 创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
    @InjectMocks
    private IndexServiceImpl service;
    
    @Before
    public void beforeTest() throws NoSuchFieldException, IllegalAccessException {
        System.out.println("========= beforeTest() ==========");
        // 其中this就是单元测试所在的类,在initMocks函数中Mockito会根据类中不同的注解(如@Mock, @Spy等)创建不同的mock对象,即初始化工作
        PowerMockito.mockStatic(StaticUtils.class);
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void getPerson() {
        String userId = "3423423113";
        String userName = "xiaoming";
        FirstEntity firstEntity = new FirstEntity();
        firstEntity.setUserId(userId);
        firstEntity.setUserName(userName);
        when(firstRepository.findFirstByUserIdAndAndUserName(anyString(), anyString()))
                .thenReturn(firstEntity);
        // 模拟静态方法
        String rel = "rel";
        PowerMockito.when(StaticUtils.printList(anyListOf(String.class)))
                .thenReturn(rel);
        ResponseEntity<Object> person = service.getPerson(userId, userName);
        Assert.assertNotNull(person);
    }
    
}

相比较TestNG 只有部分代码不一样其他都差不多。

4. 注解说明

这里是TestNG的一些注解说明:

@BeforeClass---@AfterClass

类实例化前, 被执行, 主要用于设置环境变量等, 与SpringTestContext结合用的时候要注意, 这种情况下@autowire的bean还未实例化

@BeforeTest----@AfterTest

整个测试类开始前, 被执行, 主要用户塞值, 或者进行mock(Object)的初始化, 此方法只会运行一次

@BeforeMethod-----@AfterMethod

每个测试方法执行前, 进行调用, 和@BeforeTest的主要区别在于, 你如果需要每次清空你测试的一些上下文, 那么需要配合@AfterMethod一起使用