赞
踩
如各位希望转载或引用,请注明出处,尊重原创,谢谢。如有疑问或错误,欢迎邮件沟通。
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使用
开发过程中,很多重要的业务和关键性代码,都需要单元测试覆盖,这样能保证质量。
好的单元测试用例,能有效提高软件的质量,也方便后期的代码重构和维护。但是一般来说编写单元测试工作量很大,单元测试代码维护成本也很高,因为你业务逻辑发生了改变,代码结构发生了改变,不可能不会修改单元测试。
通常单元测试的代码,阅读难度也很高,需要理解具体的业务逻辑。建议编写单元测试时,当需要使用其他类的时候,尽量使用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。
<!-- 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 -->
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()); } }
无
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。