如各位希望转载或引用,请注明出处,尊重原创,谢谢。如有疑问或错误,欢迎邮件沟通。
gitHub地址:https://github.com/thinkingfioa
邮箱地址:[email protected]
博客地址: https://blog.csdn.net/thinking_fioa
gitHub项目地址:https://github.com/thinkingfioa/tech-summary
gitHub项目代码地址:https://github.com/thinkingfioa/tech-summary-code
单元测试覆盖率往往是检验一个系统的可靠性指标,优秀的单元测试能帮助系统提早发现问题。JAVA语言提供了非常好的单元测试框架,本文将重点介绍: 如何编写单元测试Mock+PowerMock使用
开发过程中,很多重要的业务和关键性代码,都需要单元测试覆盖,这样能保证质量。
好的单元测试用例,能有效提高软件的质量,也方便后期的代码重构和维护。但是一般来说编写单元测试工作量很大,单元测试代码维护成本也很高,因为你业务逻辑发生了改变,代码结构发生了改变,不可能不会修改单元测试。
通常单元测试的代码,阅读难度也很高,需要理解具体的业务逻辑。建议编写单元测试时,当需要使用其他类的时候,尽量使用Mock方法,做到单元测试依赖越少,后续修改和理解就更简单。
建议单元测试中的test包路径目录结构与main包中的目录结构保持一致。
建议每个单元测试类测试功能单一,仅针对性测试指定类的方法。比如文件Father.java中类名称为Father,那么我们在test新建一个相同的包结构目录,并在新建后的目录下新建FatherTest.java文件,类名为FatherTest。
单元测试中每个测试方法以testXXX()开头
package org.thinking.fioa;
public class Father {
public void growUp() throws Exception {
}
}
package org.thinking.fioa;
public class FatherTest {
public void testGrowUp() throws Exception {
}
}
JAVA语言下单元测试比不可少三个依赖包,需要配置到pom.xml下。powermock + mockito + junit。
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-module-junit4artifactId>
<version>2.0.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-api-mockito2artifactId>
<version>2.0.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>2.20.1version>
<scope>testscope>
dependency>
Java使用的Junit4常用的annotation
@BeforeClass ----- 针对所有测试,只执行一次。方法签名必须是static void
@AfterClass ----- 针对所有测试,只执行一次。方法签名必须是static void
@Before ----- 初始化方法。每个@Test测试方法前都会执行一次
@Test ----- 测试方法。每个@Test测试方法都会创建一个实例对象
@After ----- 释放资源。每个@Test测试方法后都会执行一次
@BeforeClass -> {类的构造函数 -> @Before -> @Test -> @After} , 类的构造函数 -> {@Before -> @Test -> @After} … -> @AfterClass
其中每个@Test方法执行前会创建新的XxxTest实例, 单个@Test方法执行前后会执行@Before和@After方法
assertEquals(100, x) ----- 相等
assertArrayEquals(100, x) ----- 数组相等
assertNull(x) ----- 断言为null
assertTrue(x) ----- 断言为true
assertNotEquals ----- 断言不相等
expected = Exception.class ----- 异常测试
timeout=1000 ----- 超时时间
详细代码可参考tech-summary-code
下面举例介绍四大类单元测试方法,这四类单元测试用例能基本满足大家日常编写单元测试的功能
序号 | 名称 | 说明 |
---|---|---|
1 | 基础单元测试 | 基础用例 |
2 | 使用单例设计模式 | 单例是开发中最长使用的设计模式 |
3 | 依赖其他类 | 面向对象语言,封装是一大特性 |
4 | 类对象都是私有属性 | 类的属性都是私有,或者方法是私有的,可通过反射方法来编写单元测试 |
基础单元测试是最被经常使用的
测试类CommonMethod.java有如下的对象公开方法,为每个方法编写单元测试
public class CommonMethod {
public boolean success() {
return true;
}
public int age() {
return 100;
}
public int[] arrayOfInt() {
return new int[]{1, 2, 3, 4};
}
public Object isNull() {
return null;
}
public void throwException() {
throw new NullPointerException();
}
public void timeout() throws InterruptedException {
Thread.sleep(500L);
}
}
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class CommonMethodTest {
private CommonMethod method;
/**
* 仅执行一次
*/
@BeforeClass
public static void setUpBeforeClass() {
System.out.println("Before Class.");
}
/**
* 仅执行一次
*/
@AfterClass
public static void setUpAfterClass() {
System.out.println("After Class.");
}
@Before
public void setUpBefore() {
System.out.println("Before.");
method = new CommonMethod();
}
@After
public void setUpAfter() {
System.out.println("After.");
}
@Test
public void testSuccess() {
System.out.println("testSuccess.");
assertTrue(method.success());
}
@Test
public void testAge() {
System.out.println("testAge.");
assertEquals(100, method.age());
}
@Test
public void testArrayOfInt() {
System.out.println("testArrayOfInt.");
int[] copyArray = {1, 2, 3, 4};
assertArrayEquals(copyArray, method.arrayOfInt());
}
@Test
public void testIsNull() {
System.out.println("testIsNull.");
assertNull(method.isNull());
}
@Test(expected = NullPointerException.class)
public void testThrowException() {
System.out.println("testThrowException.");
method.throwException();
}
@Test(timeout = 1000)
public void testTimeout() throws InterruptedException {
System.out.println("testTimeout.");
method.timeout();
}
}
项目中最常使用的设计模式就是单例模式,开发某个类时可能需要依赖这些单例模式。编写该类单元测试时,建议使用Mock方法构建另一个单例对象供单元测试使用,而不是直接使用代码中的单例对象。
类Line.java方法midPoint()方法使用到了单例对象MathInstance.getInstance()。我们使用Mock方式创建单例对象MathInstance。
public class Line {
private final Point p1;
private final Point p2;
public Line(Point p1, Point p2) {
this.p1 = p1;
this.p2 = p2;
}
public Point midPoint() {
if (p1 == null || p2 == null) {
throw new NullPointerException("p1 or p2 is null");
}
// 使用到单例模式
return MathInstance.getInstance().midPoint(p1, p2);
}
}
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
public class MathInstance {
public static MathInstance getInstance() {
return SingleInstanceHolder.INSTANCE;
}
public Point midPoint(Point p1, Point p2) {
throw new UnsupportedOperationException("HelloWorld not supported");
}
private static class SingleInstanceHolder {
private static final MathInstance INSTANCE = new MathInstance();
}
}
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest({MathInstance.class})
@PowerMockIgnore({"javax.management.*", "javax.script.*"})
public class LineTest {
private Line line;
@Mock
private Point p1;
@Mock
private Point p2;
@Mock
private Point mid;
/**
* Mock一个单例对象
**/
@Mock
private MathInstance mathInstance;
@Before
public void setUp() {
when(mid.getX()).thenReturn(5);
when(mid.getX()).thenReturn(50);
line = new Line(p1, p2);
// 单例静态方法进行打桩,供单元测试使用
mockStatic(MathInstance.class);
when(MathInstance.getInstance()).thenReturn(mathInstance);
when(mathInstance.midPoint(p1, p2)).thenReturn(mid);
}
@Test(expected = NullPointerException.class)
public void testMidPointOfNull() {
Line localLine = new Line(null, null);
localLine.midPoint();
}
@Test
public void testMidPoint() {
assertEquals(mid, line.midPoint());
verify(mathInstance, times(1)).midPoint(p1, p2);
}
}
我们编写类A时候,可能会使用到类B的对象,也就是类A会将类B封装到自己的内部,作为自己的私有属性。
通常有两种方式来实现这样的封装:
通过构造函数传参,建议直接Mock类B的对象。这样我们可以非常方便的为类B对象打桩。
类PowerController通过构造函数参入参数PowerService对象
public class PowerController {
private final PowerService service;
// 参数传入
public PowerController(PowerService service) {
this.service = service;
}
public void saveUser(List<String> userList) {
if (null == userList || userList.isEmpty()) {
throw new IllegalArgumentException("userList is empty");
}
service.saveUser(userList);
}
public int deleteUser() {
return service.deleteUser();
}
}
public class PowerService {
public void saveUser(List<String> userList) {
throw new UnsupportedOperationException("not supported");
}
public int deleteUser() {
throw new UnsupportedOperationException("not supported");
}
}
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
public class PowerControllerTest {
private static final String USER_NAME = "thinking_fioa";
/**
* Mock一个参数
**/
@Mock
private PowerService service;
private PowerController controller;
@Before
public void setUp() {
controller = new PowerController(service);
}
@Test
public void testSaveUser() {
List<String> userList = new ArrayList<>();
userList.add(USER_NAME);
controller.saveUser(userList);
verify(service, times(1)).saveUser(anyList());
}
@Test(expected = IllegalArgumentException.class)
public void testSaveUserOfEmpty() {
List<String> userList = new ArrayList<>();
controller.saveUser(userList);
}
@Test
public void testDeleteUser() {
// 为Mock的对象打桩
when(service.deleteUser()).thenReturn(987);
assertEquals(987, controller.deleteUser());
}
}
如果是在类A中直接创建出类B的对象,而不是通过构造函数传参。我们需要使用PowerMockito.whenNew来实现打桩。
public class ConstructorMethod {
private final InnerService service;
public ConstructorMethod() {
// 内部直接创建
service = new InnerService();
}
public void sayWords(List<String> words) {
if (null == words || words.isEmpty()) {
throw new IllegalArgumentException("words is empty");
}
for (String word : words) {
service.sayWord(word);
}
}
public int removeWords() {
return service.removeWords();
}
}
public class InnerService {
public void sayWord(String word) {
throw new UnsupportedOperationException("not supported");
}
public int removeWords() {
throw new UnsupportedOperationException("not supported");
}
}
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ConstructorMethod.class})
@PowerMockIgnore({"javax.management.*", "javax.script.*"})
public class ConstructorMethodTest {
private static final String WORD = "ppp";
private static final String WORD2 = "courage";
private ConstructorMethod method;
@Mock
private InnerService service;
@Before
public void setUp() throws Exception {
// 打桩类InnerService的构造函数方法
whenNew(InnerService.class).withAnyArguments().thenReturn(service);
method = new ConstructorMethod();
}
@Test
public void testSayWords() {
List<String> words = new ArrayList<>();
words.add(WORD);
words.add(WORD2);
method.sayWords(words);
verify(service, times(words.size())).sayWord(anyString());
}
@Test(expected = IllegalArgumentException.class)
public void testSaveUserOfEmpty() {
List<String> words = new ArrayList<>();
method.sayWords(words);
}
@Test
public void testDeleteUser() {
// 打桩
when(service.removeWords()).thenReturn(1987);
assertNotEquals(987, method.removeWords());
}
}
Java语言开发时,通常会将属性设置为private,这种情况下,可以通过反射方式来实现赋值,供单元测试使用。
public class ReflectMethod {
private String name;
private int age;
private ReflectMethod(String name, int age) {
throw new UnsupportedOperationException("not ready");
}
public ReflectMethod() {
}
/**
* 成年人
*
* @return
*/
public boolean isAdult() {
return age >= 18 && null != name;
}
}
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.powermock.reflect.Whitebox;
public class ReflectMethodTest {
private static final String NAME = "thinking_fioa";
private static final int AGE = 19;
@Test(expected = UnsupportedOperationException.class)
public void testConstructor() throws Exception {
Whitebox.invokeConstructor(ReflectMethod.class, "ppp", 12);
}
@Test
public void testAdult() {
ReflectMethod method = new ReflectMethod();
// 反射赋值
Whitebox.setInternalState(method, "name", NAME);
Whitebox.setInternalState(method, "age", AGE);
assertTrue(method.isAdult());
}
@Test
public void testNotAdult() {
ReflectMethod method = new ReflectMethod();
Whitebox.setInternalState(method, "name", NAME);
Whitebox.setInternalState(method, "age", 14);
assertFalse(method.isAdult());
}
}
无