junit4整合PowerMockito进行单元测试

一、介绍

在单元测试中,代码里面往往有一些需要连接数据库、调用第三方远程的代码。

由于没有环境,这些代码的存在,会给单元测试造成影响。

所以我们在单测中,往往会使用mock的方式对这些代码做一个数据的模拟,从而达到对代码进行测试的一个目的。

所以单测需要满足以下几点

  • 可复用:单测代码可以重复执行

  • 无环境:不要依赖数据库,第三方接口等外部的环境依赖

  • 方法级细粒度:单测代码应该针对具体一个方法的测试,

  • 高覆盖率:如果代码中复杂度过高,单测要覆盖到方法中的每一行代码

  • 自动断言:每一段单测代码都应该有自己的断言方法,而不是通过打印再人工查看正确性

所以我们就有了Mockito,它可以模拟对象,模拟对象方法的返回值,来完成mock

本文使用的是PowerMockito,它是由Mockito的基础上开发而来,语法规则基本一致,同时也有一些自己的增强,可以对静态方法,局部变量进行mock

二、初步入门

假设我们有下面这两段代码PowerMockitoServiceImpl.javaPowerMockitoMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import com.banmoon.test.service.PowerMockitoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

@Autowired
private PowerMockitoMapper powerMockitoMapper;

@Override
@Transactional(rollbackFor = Exception.class)
public void insert(PowerMockitoEntity entity) {
Boolean status = Optional.ofNullable(entity.getValue()).map(a -> Boolean.TRUE).orElse(Boolean.FALSE);
entity.setStatus(status);
powerMockitoMapper.insert(entity);
}

}
1
2
3
4
5
6
7
8
package com.banmoon.test.mapper;

import com.banmoon.test.entity.PowerMockitoEntity;

public interface PowerMockitoMapper {

int insert(PowerMockitoEntity entity);
}

还有一段PowerMockitoEntity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.banmoon.test.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("power_mockito")
public class PowerMockitoEntity {

@TableId
private Integer id;

private String value;

private Boolean status;

}

上面代码所做的功能就是,插入一个实体至数据库。

在插入前,我们根据entity.value是否有值,给予entity.status的值


故此,上面的代码需要连接数据库,我们在单测时,直接对PowerMockitoMapper进行mock即可

首先,先导入依赖,根据自己的需要进行删减使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- powermock -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</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-module-junit4-rule</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-classloading-xstream</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
public class PowerMockitoServiceImplTest {

@Mock
private PowerMockitoMapper mockPowerMockitoMapper;

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

/**
* 有值测试
*/
@Test
public void testInsert1() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue("有值测试");
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

// 执行测试
powerMockitoServiceImplUnderTest.insert(entity);

// 校验结果
Assert.assertTrue(entity.getStatus());
}

/**
* 无值测试
*/
@Test
public void testInsert2() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue(null);
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

// 执行测试
powerMockitoServiceImplUnderTest.insert(entity);

// 校验结果
Assert.assertFalse(entity.getStatus());
}
}

执行结果如下,保证两个测试方法如预期通过即可

image-20230327213425469

三、其他使用

1)如何对无返回值的方法进行断言

假设有一个无返回值的方法,我们要针对它进行测试。由于它没有返回值,就没有办法对其返回值进行断言校验。

那么针对这种情况,一个方法,就算是无返回值的情况。内部一定做了一些什么操作。所以我们一般有两种方式

  • 这个方法做了设置某个对象的属性,我们可以校验对象属性是否符合预期

    • 如第二章的初步使用就是如此
  • 如果这个方法执行了某段逻辑分支的代码,我们可以可以校验那段代码是否执行过

PowerMockitoServiceImpl.java上再加一个无返回值的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

@Autowired
private PowerMockitoMapper powerMockitoMapper;

@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(PowerMockitoEntity entity) {
if (entity.getId() == null) {
powerMockitoMapper.insert(entity);
} else {
powerMockitoMapper.updateById(entity);
}
}
}

PowerMockitoMapper.java如下

1
2
3
4
5
6
7
8
9
10
package com.banmoon.test.mapper;

import com.banmoon.test.entity.PowerMockitoEntity;

public interface PowerMockitoMapper {

int insert(PowerMockitoEntity entity);

int updateById(PowerMockitoEntity entity);
}

那么我们可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
public class NoneReturnTest {

@Mock
private PowerMockitoMapper mockPowerMockitoMapper;

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

/**
* 有ID测试
*/
@Test
public void test1() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue("测试");
when(mockPowerMockitoMapper.updateById(entity)).thenReturn(1);

// 执行测试
powerMockitoServiceImplUnderTest.saveOrUpdate(entity);

// 校验结果
Mockito.verify(mockPowerMockitoMapper).updateById(entity);
}

/**
* 没有ID测试
*/
@Test
public void test2() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(null);
entity.setValue("测试");
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

// 执行测试
powerMockitoServiceImplUnderTest.saveOrUpdate(entity);

// 校验结果
Mockito.verify(mockPowerMockitoMapper).insert(entity);
}
}

2)对属局部对象进行mock并设置

如果一个方法中,有一个自己实例化的一个局部变量,那么我们该如何对其进行mock呢?

例如下面这个方法,有一个自己的局部变量tuple,并返回了这个局部变量的数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;

import java.io.File;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

@Override
public long localVariable() {
File file = new File("E://abc.txt");
return file.length();
}

}

我们只需要这样进行,即可以完成对局部变量的mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.File;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class LocalVariableTest {

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

@Test
public void localVariableTest() throws Exception {
// 设置参数
File file = PowerMockito.mock(File.class);

// mock
PowerMockito.whenNew(File.class)
.withAnyArguments()
.thenReturn(file);
PowerMockito.when(file.length()).thenReturn(2L);

// 执行测试
long i = powerMockitoServiceImplUnderTest.localVariable();

// 校验结果
Assert.assertEquals(2L, i);
}

}

3)对静态方法mock

如何对静态方法的返回值进行mock

先在PowerMockitoServiceImpl.java添加一个静态方法,其中发现HttpUtil.get()是一个静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

@Autowired
private PowerMockitoMapper powerMockitoMapper;

@Override
public int syncPowerMockitoEntity() {
String result = HttpUtil.get("url");
if (CharSequenceUtil.isNotBlank(result)) {
List<JSONObject> list = JSON.parseObject(result, List.class);
int i = 0;
for (JSONObject json : list) {
PowerMockitoEntity entity = json.toJavaObject(PowerMockitoEntity.class);
i += powerMockitoMapper.insert(entity);
}
return i;
} else {
throw new BanmoonException(1001, "同步出现异常");
}
}

}

针对上面的方法,我们可以这样进行mock,注意@PrepareForTest注解,一定要写上,改变了其中的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.banmoon.test.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
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.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest(HttpUtil.class)
public class StaticMethodTest {

@Mock
private PowerMockitoMapper mockPowerMockitoMapper;

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

/**
* 静态方法mock
*/
@Test
public void test() {
// 设置参数
final PowerMockitoEntity entity1 = new PowerMockitoEntity();
entity1.setId(1);
entity1.setValue("测试1");
final PowerMockitoEntity entity2 = new PowerMockitoEntity();
entity2.setId(2);
entity2.setValue("测试2");
List<PowerMockitoEntity> list = CollUtil.newArrayList(entity1, entity2);

// mock
PowerMockito.mockStatic(HttpUtil.class);
when(HttpUtil.get(any())).thenReturn(JSON.toJSONString(list));
when(mockPowerMockitoMapper.insert(any())).thenReturn(1);

// 执行测试
int i = powerMockitoServiceImplUnderTest.syncPowerMockitoEntity();

// 校验结果
Assert.assertEquals(2, i);
}
}

4)mock final修饰的类和方法

首先我们先写一个工具类,这个工具类是final修饰的,里面的方法也是final

1
2
3
4
5
6
7
8
9
10
11
package com.banmoon.util;

import cn.hutool.core.util.RandomUtil;

public final class PowerMockitoUtil {

public final String finalMethod(int length) {
return RandomUtil.randomString(length);
}

}

单测这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import com.banmoon.util.PowerMockitoUtil;
import org.springframework.stereotype.Service;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

private final PowerMockitoUtil powerMockitoUtil = new PowerMockitoUtil(10);

@Override
public int finalMethod() {
String method = powerMockitoUtil.finalMethod();
return method.length();
}

}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import com.banmoon.util.PowerMockitoUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.lang.reflect.Field;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class, PowerMockitoUtil.class})
public class FinalMethodTest {

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

@Test
public void finalMethodTest() throws Exception {
// 设置参数
PowerMockitoUtil util = PowerMockito.mock(PowerMockitoUtil.class);

// mock
Field field = PowerMockito.field(PowerMockitoServiceImpl.class, "powerMockitoUtil");
field.set(powerMockitoServiceImplUnderTest, util);
PowerMockito.when(util.finalMethod()).thenReturn("abcde");

// 执行测试
int i = powerMockitoServiceImplUnderTest.finalMethod();

// 校验结果
Assert.assertEquals(5, i);
}

}

5)异常的情况

有些时候,代码是会发生异常的,那么在单测的环境下,我们需要判断这些异常是什么,是不是符合预期

如下这个方法,我们只需要传个null,就会发生NullPointException的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

@Override
public int exceptionMethod(String name) {
return name.length();
}

}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class ExceptionMethodTest {

@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void exceptionMethodTest() throws Exception {
// 校验结果
Assert.assertThrows(NullPointerException.class, () -> {
// 执行测试
int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
});
}

@Test
public void exceptionMethodTest2() throws Exception {
// 校验结果
thrown.expect(NullPointerException.class);

// 执行测试
int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
}

}

四、最后

推荐一个很好用的IDEA插件,这个插件可以快速生成单元测试代码

squaretest

我是半月,你我一同共勉!!!