如何更好的编写JAVA单元测试

如各位希望转载或引用,请注明出处,尊重原创,谢谢。如有疑问或错误,欢迎邮件沟通。gitHub地址:https://github.com/thinkingfioa
邮箱地址:thinking_fioa@163.com
博客地址: https://blog.csdn.net/thinking_fioa
gitHub项目地址:https://github.com/thinkingfioa/tech-summary
gitHub项目代码地址:https://github.com/thinkingfioa/tech-summary-code

本文重点

单元测试覆盖率往往是检验一个系统的可靠性指标,优秀的单元测试能帮助系统提早发现问题。JAVA语言提供了非常好的单元测试框架,本文将重点介绍: 如何编写单元测试Mock+PowerMock使用

1. 背景

开发过程中,很多重要的业务和关键性代码,都需要单元测试覆盖,这样能保证质量。

好的单元测试用例,能有效提高软件的质量,也方便后期的代码重构和维护。但是一般来说编写单元测试工作量很大,单元测试代码维护成本也很高,因为你业务逻辑发生了改变,代码结构发生了改变,不可能不会修改单元测试。

通常单元测试的代码,阅读难度也很高,需要理解具体的业务逻辑。建议编写单元测试时,当需要使用其他类的时候,尽量使用Mock方法,做到单元测试依赖越少,后续修改和理解就更简单。

2. 编写单元测试的原则

2.1 单元测试类包路径管理

建议单元测试中的test包路径目录结构与main包中的目录结构保持一致。

2.2 单元测试类名称管理

建议每个单元测试类测试功能单一,仅针对性测试指定类的方法。比如文件Father.java中类名称为Father,那么我们在test新建一个相同的包结构目录,并在新建后的目录下新建FatherTest.java文件,类名为FatherTest。

单元测试中每个测试方法以testXXX()开头

Father.java
package org.thinking.fioa;public class Father {public void growUp() throws Exception {}
}
FatherTest.java
package org.thinking.fioa;public class FatherTest {public void testGrowUp() throws Exception {}
}

3. POM依赖

JAVA语言下单元测试比不可少三个依赖包,需要配置到pom.xml下。powermock + mockito + junit。

<!-- unit --><dependency><groupId>org.powermock</groupId><artifactId>powermock-module-junit4</artifactId><version>2.0.2</version><scope>test</scope></dependency><dependency><groupId>org.powermock</groupId><artifactId>powermock-api-mockito2</artifactId><version>2.0.2</version><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>2.20.1</version><scope>test</scope></dependency>
<!-- unit end -->

4. 基础知识

Java使用的Junit4常用的annotation

4.1 静态方法

@BeforeClass ----- 针对所有测试,只执行一次。方法签名必须是static void
@AfterClass ----- 针对所有测试,只执行一次。方法签名必须是static void

4.2 非静态方法

@Before ----- 初始化方法。每个@Test测试方法前都会执行一次
@Test ----- 测试方法。每个@Test测试方法都会创建一个实例对象
@After ----- 释放资源。每个@Test测试方法后都会执行一次

4.3 执行顺序

@BeforeClass -> {类的构造函数 -> @Before -> @Test -> @After} , 类的构造函数 -> {@Before -> @Test -> @After} … -> @AfterClass

其中每个@Test方法执行前会创建新的XxxTest实例, 单个@Test方法执行前后会执行@Before和@After方法

4.4 断言的常方法

assertEquals(100, x) ----- 相等
assertArrayEquals(100, x) ----- 数组相等
assertNull(x) ----- 断言为null
assertTrue(x) ----- 断言为true
assertNotEquals ----- 断言不相等
expected = Exception.class ----- 异常测试
timeout=1000 ----- 超时时间

5. 单元测试编写参考用例

详细代码可参考tech-summary-code

下面举例介绍四大类单元测试方法,这四类单元测试用例能基本满足大家日常编写单元测试的功能

序号 名称 说明
1 基础单元测试 基础用例
2 使用单例设计模式 单例是开发中最长使用的设计模式
3 依赖其他类 面向对象语言,封装是一大特性
4 类对象都是私有属性 类的属性都是私有,或者方法是私有的,可通过反射方法来编写单元测试

5.1 基础单元测试用例

基础单元测试是最被经常使用的

5.1.1 类CommonMethod.java

测试类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);}
}

5.1.2 测试类CommonMethodTest.java

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;/*** 仅执行一次*/@BeforeClasspublic static void setUpBeforeClass() {System.out.println("Before Class.");}/*** 仅执行一次*/@AfterClasspublic static void setUpAfterClass() {System.out.println("After Class.");}@Beforepublic void setUpBefore() {System.out.println("Before.");method = new CommonMethod();}@Afterpublic void setUpAfter() {System.out.println("After.");}@Testpublic void testSuccess() {System.out.println("testSuccess.");assertTrue(method.success());}@Testpublic void testAge() {System.out.println("testAge.");assertEquals(100, method.age());}@Testpublic void testArrayOfInt() {System.out.println("testArrayOfInt.");int[] copyArray = {1, 2, 3, 4};assertArrayEquals(copyArray, method.arrayOfInt());}@Testpublic 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();}
}

5.2 Mock一个单例对象

项目中最常使用的设计模式就是单例模式,开发某个类时可能需要依赖这些单例模式。编写该类单元测试时,建议使用Mock方法构建另一个单例对象供单元测试使用,而不是直接使用代码中的单例对象。

5.2.1 编写的类

类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();}
}

5.2.2 单元测试类

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;@Mockprivate Point p1;@Mockprivate Point p2;@Mockprivate Point mid;/*** Mock一个单例对象**/@Mockprivate MathInstance mathInstance;@Beforepublic 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();}@Testpublic void testMidPoint() {assertEquals(mid, line.midPoint());verify(mathInstance, times(1)).midPoint(p1, p2);}
}

5.3 依赖别的类的单元测试

我们编写类A时候,可能会使用到类B的对象,也就是类A会将类B封装到自己的内部,作为自己的私有属性。

通常有两种方式来实现这样的封装:

  1. 类A的构造函数中有一个参数是类B,通过传参传入
  2. 类A中直接创建类B的对象

5.3.1 通过构造函数传参数

通过构造函数传参,建议直接Mock类B的对象。这样我们可以非常方便的为类B对象打桩。

5.3.1.1 编写的类

类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");}
}
5.3.1.2 单元的类
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一个参数**/@Mockprivate PowerService service;private PowerController controller;@Beforepublic void setUp() {controller = new PowerController(service);}@Testpublic 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);}@Testpublic void testDeleteUser() {// 为Mock的对象打桩when(service.deleteUser()).thenReturn(987);assertEquals(987, controller.deleteUser());}
}

5.3.2 对象内部直接创建类B

如果是在类A中直接创建出类B的对象,而不是通过构造函数传参。我们需要使用PowerMockito.whenNew来实现打桩。

5.3.2.1 编写的类
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");}
}
5.3.2.2 单元测试的类
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;@Mockprivate InnerService service;@Beforepublic void setUp() throws Exception {// 打桩类InnerService的构造函数方法whenNew(InnerService.class).withAnyArguments().thenReturn(service);method = new ConstructorMethod();}@Testpublic 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);}@Testpublic void testDeleteUser() {// 打桩when(service.removeWords()).thenReturn(1987);assertNotEquals(987, method.removeWords());}
}

5.4 基于反射的单元测试

Java语言开发时,通常会将属性设置为private,这种情况下,可以通过反射方式来实现赋值,供单元测试使用。

5.4.1 编写的类

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;}
}

5.4.2 单元测试的类

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);}@Testpublic void testAdult() {ReflectMethod method = new ReflectMethod();// 反射赋值Whitebox.setInternalState(method, "name", NAME);Whitebox.setInternalState(method, "age", AGE);assertTrue(method.isAdult());}@Testpublic void testNotAdult() {ReflectMethod method = new ReflectMethod();Whitebox.setInternalState(method, "name", NAME);Whitebox.setInternalState(method, "age", 14);assertFalse(method.isAdult());}
}

参考资料

教你如何更好的编写JAVA单元测试相关推荐

  1. 使用JMockit编写java单元测试

    之前<有效使用Mock编写java单元测试>一文中层介绍过使用EasyMock和PowerMock来编写java单元测试,今天介绍一个更加强大的工具--JMockit. 引用单元测试中mo ...

  2. 在 Eclipse Galileo 中更快地编写 Java 代码使用新的 toString() 生成器

    http://www.ibm.com/developerworks/cn/opensource/os-eclipse-codegen/ 这个代码生成技巧使用 Eclipse Galileo 中的新特性 ...

  3. java build path entries 为空_同事的代码简直没法看,我来教你如何更优雅的设计Java异常...

    点击上方蓝色字体,选择"设为星标" 回复"666"获取面试宝典 异常处理是程序开发中必不可少操作之一,但如何正确优雅的对异常进行处理确是一门学问,笔者根据自己的 ...

  4. 教你如何更优雅的设计Java异常

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:lrwinx https://lrwinx.github.i ...

  5. 如何编写Java单元测试(TC)?

    被测类 public class Student {@Autowiredprivate Card card;public boolean judgeNumber(Long uid) throws Se ...

  6. 如何更规范化编写Java 代码

    作者: 涛姐涛哥 出处:https://www.cnblogs.com/taojietaoge/p/11575376.html 原文: https://www.cnblogs.com/taojieta ...

  7. 用vs2017编写html,vs2017可以编写java

    vs2017可以编写java [2021-02-06 06:57:17]  简介: 服务器 Dockerfile这个东西我们之前是介绍过,它方便,快捷,易用,而在vs2017中也对docker进行了支 ...

  8. IT兄弟连 Java语法教程 编写Java源代码

    现在我们来一步一步的编写第一个Java程序,鼎鼎大名的"HelloWorld". 编写Java源代码 编写Java源代码可以使用任何无格式的文本编辑器,在Windows操作系统上可 ...

  9. java云端开发_云端编写Java代码的方法

    云端编写Java代码的方法 通常情况下,正如云厂商和虚拟计算提供商所宣传的转移到云端是一个无缝的流程,经验却告诉我们过于猛烈地脱离内部数据中心,部署到云端是一种复杂的方式.从部署流程到方式的所有事情, ...

最新文章

  1. 【C++】对象实例化/成员函数/成员变量的内存管理
  2. Spring基础专题——第三章(反转控制与依赖注入)
  3. python pytz 获取指定时区的时间
  4. X皮书之shell 常用代码
  5. Linux:WPS不能使用中文输入法
  6. CF436F Banners(分块/凸包/单调队列)
  7. memcached操作
  8. SVM中的间隔最大化
  9. python 爬虫库 u_9个用来爬取网络站点的 Python 库
  10. mac配置OpenGL超级宝典(第7版)环境
  11. python 基于smb通信协议实现NAS服务器文件上传和下载
  12. OpenWrt 一个关于IPv6巨傻X的设置项
  13. win11 PL2303驱动问题解决
  14. BDB(ICCV2019)
  15. 文字转语音:CyberBukit TTS for php 1.2.3
  16. 网上看到一个提供WebService的地方,如果作webService测试,不妨试试
  17. 疯子网页采集器教程之采集需要保存图片的教程
  18. JAVA资深架构师成长路线-架构师筑基必备技能-深入Tomcat底层
  19. 无胁科技-TVD每日漏洞情报-2022-11-21
  20. 15.7.1压缩文件

热门文章

  1. MATLAB音乐合成——小星星
  2. jquery flexslider轮播
  3. 字节跳动面试,两面+HR面,面试官很棒!
  4. 什么是网络?网络的基本框架是什么?
  5. 对比九毛九、海底捞,绿茶餐厅值得投资吗?
  6. 米兔积木机器人与履带机甲零件差别_米兔积木机器人履带机甲 给你一个智能的选择...
  7. 电磁兼容(EMC)基础知识
  8. webpack基础学习,各个loader和plugin的具体配置
  9. CSS图像填充文字(镂空文字效果 / 文字镂空效果)
  10. 中国移动敲定A股发行价,预计上市时市值将达到1.18万亿元