主导目标:单元测试覆盖率100%
一、主要方法
- 引用junit类库
- 引用Mockito、PowerMockito等Mock类库模拟被测类、模拟被测试类的成员、模拟方法返回
- 使用反射修改被测类私有字段以及静态字段
- 继承被测类生成Demo子类,可以用于改写protected成员方法
- 模拟嵌套调用
二、包依赖
本文例子基于
<spring-boot.version>2.6.12</spring-boot.version>
- spring-boot-starter-test包依赖,其下包含了junit-jupiter、mockito等包
<!-- spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring-boot-starter-test中包含了junit-jupiter、mockito等包 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
- powermock包依赖
<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>
三、示例
Mockito使用
// mock
FooRepo mockRepo = Mockito.mock(FooRepo.class);
// when
Mockito.when(mockRepo.getById(1)).thenReturn(entity);
// doAnswer
Mockito.doAnswer(invocationOnMock -> {
try {
Thread.sleep(200); // 模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
return "mockAccessToken";
}).when(mockRepo).getById(1);
被测类Foo
package com.example;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Foo {
/**
* DB中AccessToken过期时间91分钟(必须大于Cache过期时间)
*/
private static final int DB_EXPIRED_MINUTES = 91;
@Autowired
FooRepo repo;
public String getName(int id, String prefix) {
var student = repo.getById(id);
if (student == null) {
return null;
}
var name = student.getName();
if (!isNameBeginWith(name, prefix)) {
return null;
}
return name;
}
protected boolean isNameBeginWith(String name, String prefix) {
return name.startsWith(prefix);
}
public boolean setName(int id, String name) {
return repo.update(id, name);
}
@Data
static class Student {
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
static class FooRepo {
public Student getById(int id) {
return new Student(1, "Lxx");
}
public boolean update(int id, String name) {
return true;
}
}
}
单元测试类FooTest
package com.example;
import com.exampleUnitTestUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class FooTest {
Foo subject;
FooDemo demoSubject = new FooDemo();
Foo.FooRepo mockRepo = Mockito.mock(Foo.FooRepo.class);
@BeforeEach
void setUp() throws NoSuchFieldException {
subject = new Foo();
// 使用反射修复private字段
UnitTestUtil.setField(subject, "repo", mockRepo);
UnitTestUtil.setField(demoSubject, Foo.class, "repo", mockRepo);
var entity = buildEntity();
var entity2 = buildEntity();
entity2.setId(2);
entity2.setName("mockName2");
Mockito.when(mockRepo.getById(1)).thenReturn(entity);
Mockito.when(mockRepo.getById(2)).thenReturn(entity2);
}
private Foo.Student buildEntity() {
return new Foo.Student(1, "mockName");
}
@Test
void getById() {
var name = subject.getName(1, "m");
assertEquals("mockName", name);
}
@Test
void getByIdNull() {
var name = subject.getName(2, "L");
assertNull(name);
}
@Test
void getByIdDemo() {
var name = demoSubject.getName(1, "m");
assertEquals("mockName", name);
}
/**
* 使用Demo子类改写被测类protected成员方法
*/
static class FooDemo extends Foo {
@Override
protected boolean isNameBeginWith(String name, String prefix) {
if ("L".equals(prefix)) {
return true;
}
return super.isNameBeginWith(name, prefix);
}
}
}
关于模拟嵌套调用,比如下面的tokenFactory.getContainer().getToken()
。
可以先使用反射来mock掉tokenFactory,再使用when来mock掉getContainer()的返回值
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class Foo{
FooTokenFactory tokenFactory = SpringContextUtil.getBean(FooTokenFactory.class);
public Token getToken() {
var result = tokenFactory.getContainer().getToken();
return new Token(result.getValue());
}
}
class FooTest{
// 第1步mock创建新factory
FooTokenFactory tokenFactory = mock(FooTokenFactory.class);
@BeforeEach
void setUp() throws NoSuchFieldException {
// 第2步mock掉被测类中的factory成员
UnitTestUtil.setField(subject, "tokenFactory", tokenFactory);
var containerDemo = new FooTokenFactoryDemo();
// 第4步mock掉factory方法调用
when(tokenFactory.getContainer()).thenReturn(containerDemo);
}
@Test
void getToken() {
var token = subject.getToken();
assertEquals("mockToken", token.getValue());
}
/** 第3步子类方式改写嵌套返回 **/
static class FooTokenFactoryDemo extends FooTokenFactory {
@Override
public Token getToken() {
return new Token("mockToken");
}
}
}
四、其他
工具类UnitTestUtil:使用反射修改被测类的私有字段
注意要在子类对象上修改基类字段,需要将基类class对象来查找getDeclaredField
import org.springframework.util.ReflectionUtils;
public class UnitTestUtil {
private UnitTestUtil() {
}
public static void setField(Object target, String fieldName, Object mockObject) throws NoSuchFieldException {
setField(target, target.getClass(), fieldName, mockObject);
}
public static void setStaticField(Class<?> clazz, String fieldName, Object mockObject) throws NoSuchFieldException {
setField(null, clazz, fieldName, mockObject);
}
public static void setField(Object target, Class<?> clazz, String fieldName, Object mockObject) throws NoSuchFieldException {
var field = clazz.getDeclaredField(fieldName);
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, target, mockObject);
}
}
网友评论