教你如何更好的编写JAVA单元测试
如何更好的編寫JAVA單元測(cè)試
如各位希望轉(zhuǎn)載或引用,請(qǐng)注明出處,尊重原創(chuàng),謝謝。如有疑問(wèn)或錯(cuò)誤,歡迎郵件溝通。gitHub地址:https://github.com/thinkingfioa 郵箱地址:thinking_fioa@163.com 博客地址: https://blog.csdn.net/thinking_fioa gitHub項(xiàng)目地址:https://github.com/thinkingfioa/tech-summary gitHub項(xiàng)目代碼地址:https://github.com/thinkingfioa/tech-summary-code本文重點(diǎn)
單元測(cè)試覆蓋率往往是檢驗(yàn)一個(gè)系統(tǒng)的可靠性指標(biāo),優(yōu)秀的單元測(cè)試能幫助系統(tǒng)提早發(fā)現(xiàn)問(wèn)題。JAVA語(yǔ)言提供了非常好的單元測(cè)試框架,本文將重點(diǎn)介紹: 如何編寫單元測(cè)試Mock+PowerMock使用
1. 背景
開(kāi)發(fā)過(guò)程中,很多重要的業(yè)務(wù)和關(guān)鍵性代碼,都需要單元測(cè)試覆蓋,這樣能保證質(zhì)量。
好的單元測(cè)試用例,能有效提高軟件的質(zhì)量,也方便后期的代碼重構(gòu)和維護(hù)。但是一般來(lái)說(shuō)編寫單元測(cè)試工作量很大,單元測(cè)試代碼維護(hù)成本也很高,因?yàn)槟銟I(yè)務(wù)邏輯發(fā)生了改變,代碼結(jié)構(gòu)發(fā)生了改變,不可能不會(huì)修改單元測(cè)試。
通常單元測(cè)試的代碼,閱讀難度也很高,需要理解具體的業(yè)務(wù)邏輯。建議編寫單元測(cè)試時(shí),當(dāng)需要使用其他類的時(shí)候,盡量使用Mock方法,做到單元測(cè)試依賴越少,后續(xù)修改和理解就更簡(jiǎn)單。
2. 編寫單元測(cè)試的原則
2.1 單元測(cè)試類包路徑管理
建議單元測(cè)試中的test包路徑目錄結(jié)構(gòu)與main包中的目錄結(jié)構(gòu)保持一致。
2.2 單元測(cè)試類名稱管理
建議每個(gè)單元測(cè)試類測(cè)試功能單一,僅針對(duì)性測(cè)試指定類的方法。比如文件Father.java中類名稱為Father,那么我們?cè)趖est新建一個(gè)相同的包結(jié)構(gòu)目錄,并在新建后的目錄下新建FatherTest.java文件,類名為FatherTest。
單元測(cè)試中每個(gè)測(cè)試方法以testXXX()開(kāi)頭
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語(yǔ)言下單元測(cè)試比不可少三個(gè)依賴包,需要配置到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. 基礎(chǔ)知識(shí)
Java使用的Junit4常用的annotation
4.1 靜態(tài)方法
@BeforeClass ----- 針對(duì)所有測(cè)試,只執(zhí)行一次。方法簽名必須是static void
@AfterClass ----- 針對(duì)所有測(cè)試,只執(zhí)行一次。方法簽名必須是static void
4.2 非靜態(tài)方法
@Before ----- 初始化方法。每個(gè)@Test測(cè)試方法前都會(huì)執(zhí)行一次
@Test ----- 測(cè)試方法。每個(gè)@Test測(cè)試方法都會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象
@After ----- 釋放資源。每個(gè)@Test測(cè)試方法后都會(huì)執(zhí)行一次
4.3 執(zhí)行順序
@BeforeClass -> {類的構(gòu)造函數(shù) -> @Before -> @Test -> @After} , 類的構(gòu)造函數(shù) -> {@Before -> @Test -> @After} … -> @AfterClass
其中每個(gè)@Test方法執(zhí)行前會(huì)創(chuàng)建新的XxxTest實(shí)例, 單個(gè)@Test方法執(zhí)行前后會(huì)執(zhí)行@Before和@After方法
4.4 斷言的常方法
assertEquals(100, x) ----- 相等
assertArrayEquals(100, x) ----- 數(shù)組相等
assertNull(x) ----- 斷言為null
assertTrue(x) ----- 斷言為true
assertNotEquals ----- 斷言不相等
expected = Exception.class ----- 異常測(cè)試
timeout=1000 ----- 超時(shí)時(shí)間
5. 單元測(cè)試編寫參考用例
詳細(xì)代碼可參考tech-summary-code
下面舉例介紹四大類單元測(cè)試方法,這四類單元測(cè)試用例能基本滿足大家日常編寫單元測(cè)試的功能
| 1 | 基礎(chǔ)單元測(cè)試 | 基礎(chǔ)用例 |
| 2 | 使用單例設(shè)計(jì)模式 | 單例是開(kāi)發(fā)中最長(zhǎng)使用的設(shè)計(jì)模式 |
| 3 | 依賴其他類 | 面向?qū)ο笳Z(yǔ)言,封裝是一大特性 |
| 4 | 類對(duì)象都是私有屬性 | 類的屬性都是私有,或者方法是私有的,可通過(guò)反射方法來(lái)編寫單元測(cè)試 |
5.1 基礎(chǔ)單元測(cè)試用例
基礎(chǔ)單元測(cè)試是最被經(jīng)常使用的
5.1.1 類CommonMethod.java
測(cè)試類CommonMethod.java有如下的對(duì)象公開(kāi)方法,為每個(gè)方法編寫單元測(cè)試
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 測(cè)試類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;/*** 僅執(zhí)行一次*/@BeforeClasspublic static void setUpBeforeClass() {System.out.println("Before Class.");}/*** 僅執(zhí)行一次*/@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一個(gè)單例對(duì)象
項(xiàng)目中最常使用的設(shè)計(jì)模式就是單例模式,開(kāi)發(fā)某個(gè)類時(shí)可能需要依賴這些單例模式。編寫該類單元測(cè)試時(shí),建議使用Mock方法構(gòu)建另一個(gè)單例對(duì)象供單元測(cè)試使用,而不是直接使用代碼中的單例對(duì)象。
5.2.1 編寫的類
類Line.java方法midPoint()方法使用到了單例對(duì)象MathInstance.getInstance()。我們使用Mock方式創(chuàng)建單例對(duì)象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 單元測(cè)試類
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一個(gè)單例對(duì)象**/@Mockprivate MathInstance mathInstance;@Beforepublic void setUp() {when(mid.getX()).thenReturn(5);when(mid.getX()).thenReturn(50);line = new Line(p1, p2);// 單例靜態(tài)方法進(jìn)行打樁,供單元測(cè)試使用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 依賴別的類的單元測(cè)試
我們編寫類A時(shí)候,可能會(huì)使用到類B的對(duì)象,也就是類A會(huì)將類B封裝到自己的內(nèi)部,作為自己的私有屬性。
通常有兩種方式來(lái)實(shí)現(xiàn)這樣的封裝:
5.3.1 通過(guò)構(gòu)造函數(shù)傳參數(shù)
通過(guò)構(gòu)造函數(shù)傳參,建議直接Mock類B的對(duì)象。這樣我們可以非常方便的為類B對(duì)象打樁。
5.3.1.1 編寫的類
類PowerController通過(guò)構(gòu)造函數(shù)參入?yún)?shù)PowerService對(duì)象
public class PowerController {private final PowerService service;// 參數(shù)傳入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一個(gè)參數(shù)**/@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的對(duì)象打樁when(service.deleteUser()).thenReturn(987);assertEquals(987, controller.deleteUser());} }5.3.2 對(duì)象內(nèi)部直接創(chuàng)建類B
如果是在類A中直接創(chuàng)建出類B的對(duì)象,而不是通過(guò)構(gòu)造函數(shù)傳參。我們需要使用PowerMockito.whenNew來(lái)實(shí)現(xiàn)打樁。
5.3.2.1 編寫的類
public class ConstructorMethod {private final InnerService service;public ConstructorMethod() {// 內(nèi)部直接創(chuàng)建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 單元測(cè)試的類
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的構(gòu)造函數(shù)方法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 基于反射的單元測(cè)試
Java語(yǔ)言開(kāi)發(fā)時(shí),通常會(huì)將屬性設(shè)置為private,這種情況下,可以通過(guò)反射方式來(lái)實(shí)現(xiàn)賦值,供單元測(cè)試使用。
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 單元測(cè)試的類
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());} }參考資料
無(wú)
總結(jié)
以上是生活随笔為你收集整理的教你如何更好的编写JAVA单元测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: c语言版实验1集合并交差,数据结构(C语
- 下一篇: JAVA树状结构数据处理