說明
? ? ? ? 在第一份工作中,我經(jīng)歷了我的第一個(gè)商用的項(xiàng)目,是一個(gè)微服務(wù)模塊(后臺管理模塊),我所在的團(tuán)隊(duì)做的是南方電網(wǎng)的項(xiàng)目。不扯遠(yuǎn)了,當(dāng)時(shí)我開始做的時(shí)候在想要以什么方式開發(fā)時(shí),我的組長因擔(dān)心項(xiàng)目到期出不了成品,所以催促我快點(diǎn)產(chǎn)出代碼。之后就有了這篇文章,由于我為了趕進(jìn)度而沒有做單元測試,這導(dǎo)致了項(xiàng)目的后半部分bug產(chǎn)出數(shù)量隨著代碼的增加而增加,更郁悶地是修改之后為了配合前端都得去服務(wù)器部署一次,浪費(fèi)的時(shí)間不比寫單元測試少,也讓我明白了單元測試的重要性。我的要求是讓自己學(xué)會TDD測試驅(qū)動開發(fā)的開發(fā)習(xí)慣,且寫下該文記錄自己學(xué)習(xí)的歷程。
? ? ? ? 該文將介紹單元測試框架Junit,模仿對象的框架Mockito。文章介紹核心的概念,感興趣可以進(jìn)入?yún)⒖嘉恼禄蛘邥M(jìn)一步閱讀。
參考文章以及書籍
《測試驅(qū)動開發(fā)》
Hamcrest 總結(jié)
深入JUnit源碼之Rule
JUnit4.11 理論機(jī)制 @Theory 完整解讀
Junit4.8之Category
Mockito官方文檔中文版
Junit測試Controller-RESTful接口
IDEA代碼覆蓋率測試
測試驅(qū)動開發(fā)-TDD
? ? ? ? 也有人把這種流程稱為極限編程。
? ? ? ? 測試驅(qū)動開發(fā)是使用測試框架的目的,能更好更快地讓我們寫完代碼然后傲游二次元或者是出門做個(gè)現(xiàn)充。
? ? ? ? 測試驅(qū)動開發(fā)指的是就是字面上的意思,用測試來驅(qū)動整個(gè)開發(fā)流程,也就是在寫開發(fā)代碼前先提前寫測試代碼,然后寫業(yè)務(wù)代碼來讓測試通過。這里只是簡單地概念性介紹,主要是要避免“開發(fā)后簡單地驗(yàn)證結(jié)果”而轉(zhuǎn)為“主動編寫測試用例然后編寫代碼使用例通過”。
整個(gè)流程以就是不斷重復(fù)下面幾步:
|--->快速創(chuàng)建一個(gè)測試
? ? ? ?|--->運(yùn)行所有測試,發(fā)現(xiàn)新測試無法通過
? ? ? ?? ? ? ?|--->做一些細(xì)微的調(diào)整
? ? ? ?? ? ? ?? ? ? ?|--->運(yùn)行所有測試,所有測試通過
? ? ? ??? ? ?? ? ? ?? ? ? ?|--->重構(gòu)代碼,消除重復(fù),優(yōu)化代碼結(jié)構(gòu)
Junit
JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
Junit是一個(gè)簡單的測試框架,其官網(wǎng)涵蓋大量的用例可以作為使用參考。/home/harry
Assertions-斷言機(jī)制
? ? ? ? 斷言機(jī)制是判斷結(jié)果是否正確的機(jī)制,總是以“assertXXXX”格式出現(xiàn),只要有一條驗(yàn)證結(jié)果與預(yù)想不匹配,則測試不通過。
? ? ? ? Hamcrest提供了其他的一些斷言方式,請查看Hamcrest 總結(jié)。
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
}
Runner-測試執(zhí)行器
? ? ? ? 你寫的測試代碼都會在這些測試執(zhí)行器中運(yùn)行,Junit默認(rèn)的執(zhí)行器為BlockJUnit4ClassRunner,當(dāng)你不指定時(shí),就會使用這個(gè)類來運(yùn)行測試。可以使用注解@RunWith(xxxx)來指定運(yùn)行測試的執(zhí)行器。
Suite組合測試
? ? ? ? Suite測試在Junit中是測試一組測試用例,用途比較明確,就是有些操作是需要執(zhí)行多個(gè)操作才能完成的,所以可以組成一個(gè)組來進(jìn)行測試。
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestFeatureLogin.class,
TestFeatureLogout.class,
TestFeatureNavigate.class,
TestFeatureUpdate.class
})
public class FeatureTestSuite {
// the class remains empty,
// used only as a holder for the above annotations
}
指定期待異常
? ? ? ? 如果沒有拋出指定異常則測試不通過。
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
忽略測試
? ? ? ? 可以讓測試暫時(shí)失效。
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
assertThat(1, is(1));
}
測試失效時(shí)間
@Test(timeout=1000)
public void testWithTimeout() {
...
}
Rule機(jī)制
JUnit中的Rule是對@BeforeClass、@AfterClass、@Before、@After等注解的另一種實(shí)現(xiàn),其中@ClassRule實(shí)現(xiàn)的功能和@BeforeClass、@AfterClass類似;@Rule實(shí)現(xiàn)的功能和@Before、@after類似。JUnit引入@ClassRule和@Rule注解的關(guān)鍵是想讓以前在@BeforeClass、@AfterClass、@Before、@After中的邏輯能更加方便的實(shí)現(xiàn)重用,因?yàn)锧BeforeClass、@AfterClass、@Before、@After是將邏輯封裝在一個(gè)測試類的方法中的,如果實(shí)現(xiàn)重用,需要自己將這些邏輯提取到一個(gè)單獨(dú)的類中,再在這些方法中調(diào)用,而@ClassRule、@Rule則是將邏輯封裝在一個(gè)類中,當(dāng)需要使用時(shí),直接賦值即可,對不需要重用的邏輯則可用匿名類實(shí)現(xiàn),也因此,JUnit在接下來的版本中更傾向于多用@ClassRule和@Rule。
? ? ? ? Junit @Rule和@ClassRule只能注解在字段上,且該字段必須實(shí)現(xiàn)TestRule接口,Junit提供了一些默認(rèn)實(shí)現(xiàn)類。

public class DigitalAssetManagerTest {
@Rule
public final TemporaryFolder tempFolder = new TemporaryFolder();
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void countsAssets() throws IOException {
File icon = tempFolder.newFile("icon.png");
File assets = tempFolder.newFolder("assets");
createAssets(assets, 3);
DigitalAssetManager dam = new DigitalAssetManager(icon, assets);
assertEquals(3, dam.getAssetCount());
}
private void createAssets(File assets, int numberOfAssets) throws IOException {
for (int index = 0; index < numberOfAssets; index++) {
File asset = new File(assets, String.format("asset-%d.mpg", index));
Assert.assertTrue("Asset couldn't be created.", asset.createNewFile());
}
}
@Test
public void throwsIllegalArgumentExceptionIfIconIsNull() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Icon is null, not a file, or doesn't exist.");
new DigitalAssetManager(null, null);
}
}
Theory機(jī)制
? ? ? ? Theory是一個(gè)自動化填充參數(shù)并進(jìn)行多次參數(shù)測試的一個(gè)機(jī)制,可以自己實(shí)現(xiàn)注解來定義參數(shù)。下面的代碼中,定義了兩個(gè)實(shí)參,而當(dāng)測試 filenameIncludesUsername 方法時(shí),形參username會被兩個(gè)實(shí)參填充并各執(zhí)行一次。
@RunWith(Theories.class)
public class UserTest {
@DataPoint
public static String GOOD_USERNAME = "optimus";
@DataPoint
public static String USERNAME_WITH_SLASH = "optimus/prime";
@Theory
public void filenameIncludesUsername(String username) {
assumeThat(username, not(containsString("/")));
assertThat(new User(username).configFileName(), containsString(username));
}
}
Test Fixture 測試固件
The test fixture is everything we need to have in place to exercise the SUT
Test Fixture(測試固件)是指一個(gè)測試運(yùn)行所需的固定環(huán)境。
Fixtures 是測試中非常重要的一部分。他們的主要目的是建立一個(gè)固定/已知的環(huán)境狀態(tài)以確保 測試可重復(fù)并且按照預(yù)期方式運(yùn)行。Junit提供了一些方法來設(shè)置fixture,可以用來設(shè)置測試方法所需的環(huán)境數(shù)據(jù)等,允許你精確的定義你的Fixtures。大致上分為三類:
Test Fixtures
規(guī)則(Rules&RulesClass)
Theories
大致包含了如下過程
@BeforeClass setUpClass
@Before setUp
@Test test2()
@After tearDown
@Before setUp
@Test test1()
@After tearDown
@AfterClass tearDownClass
Category機(jī)制
一種級聯(lián)的測試方式,
@IncludeCategory(XXX.class)可以包含@Category(XXX.class)
@ExcludeCategory(XXX.class)將會忽略@Category(XXX.class)
細(xì)節(jié)參照博文
Mock和Mockito
? ? ? ? mock其實(shí)是一種工具的簡稱,他最大的功能是幫你把單元測試的耦合分解開,如果你的代碼對另一個(gè)類或者接口有依賴,它能夠幫你模擬這些依賴,并幫你驗(yàn)證所調(diào)用的依賴的行為。


下面的代碼來自官網(wǎng)示例
1. 驗(yàn)證某些行為
// 靜態(tài)導(dǎo)入會使代碼更簡潔
import static org.mockito.Mockito.*;
// mock creation 創(chuàng)建mock對象
List mockedList = mock(List.class);
//using mock object 使用mock對象
mockedList.add("one");
mockedList.clear();
//verification 驗(yàn)證
verify(mockedList).add("one");
verify(mockedList).clear();
2. 制作測試樁
//You can mock concrete classes, not only interfaces
// 你可以mock具體的類型,不僅只是接口
LinkedList mockedList = mock(LinkedList.class);
//stubbing
// 測試樁
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
// 輸出“first”
System.out.println(mockedList.get(0));
//following throws runtime exception
// 拋出異常
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
// 因?yàn)間et(999) 沒有打樁,因此輸出null
System.out.println(mockedList.get(999));
//Although it is possible to verify a stubbed invocation, usually it's just redundant
//If your code cares what get(0) returns then something else breaks (often before even verify() gets executed).
//If your code doesn't care what get(0) returns then it should not be stubbed. Not convinced? See here.
// 驗(yàn)證get(0)被調(diào)用的次數(shù)
verify(mockedList).get(0);
? ? ? ? 默認(rèn)情況下,所有的函數(shù)都有返回值。mock函數(shù)默認(rèn)返回的是null,一個(gè)空的集合或者一個(gè)被對象類型包裝的內(nèi)置類型,例如0、false對應(yīng)的對象類型為Integer、Boolean。
3. 參數(shù)匹配器(ArgumentMatchers)
//stubbing using built-in anyInt() argument matcher
// 使用內(nèi)置的anyInt()參數(shù)匹配器
when(mockedList.get(anyInt())).thenReturn("element");
//stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
// 使用自定義的參數(shù)匹配器( 在isValid()函數(shù)中返回你自己的匹配器實(shí)現(xiàn) )
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
//following prints "element"
// 輸出element
System.out.println(mockedList.get(999));
//you can also verify using an argument matcher
// 你也可以驗(yàn)證參數(shù)匹配器
verify(mockedList).get(anyInt());
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher
// 上述代碼是正確的,因?yàn)閑q()也是一個(gè)參數(shù)匹配器
// 該示例展示了如何多次應(yīng)用于測試樁函數(shù)的驗(yàn)證
? ? ? ? 參數(shù)匹配器使驗(yàn)證和測試樁變得更靈活,參考API文檔。
4. 驗(yàn)證函數(shù)的確切、最少、從未調(diào)用次數(shù)
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//following two verifications work exactly the same - times(1) is used by default
// 下面的兩個(gè)驗(yàn)證函數(shù)效果一樣,因?yàn)関erify默認(rèn)驗(yàn)證的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
//exact number of invocations verification
// 驗(yàn)證具體的執(zhí)行次數(shù)
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//verification using never(). never() is an alias to times(0)
// 使用never()進(jìn)行驗(yàn)證,never相當(dāng)于times(0)
verify(mockedList, never()).add("never happened");
//verification using atLeast()/atMost()
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
5. 使用stub拋出異常
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
// 調(diào)用這句代碼會拋出異常
mockedList.clear();
6. 驗(yàn)證執(zhí)行順序
// A. Single mock whose methods must be invoked in a particular order
// A. 驗(yàn)證mock一個(gè)對象的函數(shù)執(zhí)行順序
List singleMock = mock(List.class);
//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");
//create an inOrder verifier for a single mock
// 為該mock對象創(chuàng)建一個(gè)inOrder對象
InOrder inOrder = inOrder(singleMock);
//following will make sure that add is first called with "was added first, then with "was added second"
// 確保add函數(shù)首先執(zhí)行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B. Multiple mocks that must be used in a particular order
// B .驗(yàn)證多個(gè)mock對象的函數(shù)執(zhí)行順序
List firstMock = mock(List.class);
List secondMock = mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//create inOrder object passing any mocks that need to be verified in order
// 為這兩個(gè)Mock對象創(chuàng)建inOrder對象
InOrder inOrder = inOrder(firstMock, secondMock);
//following will make sure that firstMock was called before secondMock
// 驗(yàn)證它們的執(zhí)行順序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
// Oh, and A + B can be mixed together at will
7. 確保交互(interaction)操作不會執(zhí)行在mock對象上
//using mocks - only mockOne is interacted
// 使用Mock對象
mockOne.add("one");
//ordinary verification
// 普通驗(yàn)證
verify(mockOne).add("one");
//verify that method was never called on a mock
// 驗(yàn)證某個(gè)交互是否從未被執(zhí)行
verify(mockOne, never()).add("two");
//verify that other mocks were not interacted
// 驗(yàn)證mock對象沒有交互過
verifyZeroInteractions(mockTwo, mockThree);
8. 簡化mock對象的創(chuàng)建
public class ArticleManagerTest {
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;
// 需要執(zhí)行下面語句來初始化注解的使用
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
//也可以在測試類加上注解@RunWith(MockitoJUnit44Runner.class)
//或者使用MockitoJUnitRunner
9. 為連續(xù)的調(diào)用做測試樁
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
//First call: throws runtime exception:
// 第一次調(diào)用 : 拋出運(yùn)行時(shí)異常
mock.someMethod("some arg");
//Second call: prints "foo"
// 第二次調(diào)用 : 輸出"foo"
System.out.println(mock.someMethod("some arg"));
//Any consecutive call: prints "foo" as well (last stubbing wins).
// 后續(xù)調(diào)用 : 也是輸出"foo"
System.out.println(mock.someMethod("some arg"));
// 簡短的寫法,第一次調(diào)用時(shí)返回"one",第二次返回"two",第三次返回"three"
when(mock.someMethod("some arg"))
.thenReturn("one", "two", "three");
10. 為回調(diào)做測試樁
Allows stubbing with generic Answer interface. 運(yùn)行為泛型接口Answer打樁。
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});
//Following prints "called with arguments: foo"
// 輸出 : "called with arguments: foo"
System.out.println(mock.someMethod("foo"));
11. 監(jiān)控真實(shí)對象
? ? ? ? 你可以為真實(shí)對象創(chuàng)建一個(gè)監(jiān)控(spy)對象。當(dāng)你使用這個(gè)spy對象時(shí)真實(shí)的對象也會也調(diào)用,除非它的函數(shù)被stub了。
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
// 你可以為某些函數(shù)打樁
when(spy.size()).thenReturn(100);
//using the spy calls *real* methods
// 通過spy對象調(diào)用真實(shí)對象的函數(shù)
spy.add("one");
spy.add("two");
//prints "one" - the first element of a list
// 輸出第一個(gè)元素
System.out.println(spy.get(0));
//size() method was stubbed - 100 is printed
// 因?yàn)閟ize()函數(shù)被打樁了,因此這里返回的是100
System.out.println(spy.size());
//optionally, you can verify
// 交互驗(yàn)證
verify(spy).add("one");
verify(spy).add("two");
12. 為下一步的斷言捕獲參數(shù)(ArgumentCaptor)
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
// 參數(shù)捕獲
verify(mock).doSomething(argument.capture());
// 使用equal斷言
assertEquals("John", argument.getValue().getName());
13. TDD與BDD(行為驅(qū)動開發(fā))結(jié)合使用
import static org.mockito.BDDMockito.*;
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
public void shouldBuyBread() throws Exception {
//given
given(seller.askForBread()).willReturn(new Bread());
//when
Goods goods = shop.buyBread();
//then
assertThat(goods, containBread());
}
14. @Captor,@Spy,@ InjectMocks
@Captor 簡化 ArgumentCaptor 的創(chuàng)建 - 當(dāng)需要捕獲的參數(shù)是一個(gè)令人討厭的通用類,而且你想避免編譯時(shí)警告。
@Spy - 你可以用它代替 spy(Object) 方法
@InjectMocks - 自動將模擬對象或偵查域注入到被測試對象中。需要注意的是 @InjectMocks 也能與 @Spy 一起使用,這就意味著 Mockito 會注入模擬對象到測試的部分測試中。它的復(fù)雜度也是你應(yīng)該使用部分測試原因。
15. 驗(yàn)證超時(shí)
//passes when someMethod() is called within given time span
verify(mock, timeout(100)).someMethod();
//above is an alias to:
verify(mock, timeout(100).times(1)).someMethod();
//passes when someMethod() is called *exactly* 2 times within given time span
verify(mock, timeout(100).times(2)).someMethod();
//passes when someMethod() is called *at least* 2 times within given time span
verify(mock, timeout(100).atLeast(2)).someMethod();
//verifies someMethod() within given time span using given verification mode
//useful only if you have your own custom verification modes.
verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();
以上內(nèi)容,足以讓我們使用Junit 4進(jìn)行日常代碼的單元測試
使用Junit + Mockito 對Service做單元測試
public interface RegisterUserService {
boolean insert(String passid,String msisdn,String email) throws SQLException;
}
@Service("registerUserService")
public class RegisterUserServiceImpl implements RegisterUserService {
private Logger loggor = Logger.getLogger(getClass());
@Autowired
private UserMapper userMapper;
@Autowired
private PassidUserMapper passidUserMapper;
@Autowired
@Qualifier("redisService")
private CacheService redisService;
@Override
@Transactional
public boolean insert(String passid, String msisdn, String email)
throws SQLException {
if (StringUtils.isEmpty(passid))
return false;
User user = new User();
if (!StringUtils.isEmpty(msisdn))
user.setPhoneNo(msisdn);
if (!StringUtils.isEmpty(email))
user.setEmail(email);
PassidUser passidUser = new PassidUser();
String serverCode = ServerCodeConfig.serverCodeMap
.get(PayUtil.ipAddress);
if (StringUtils.isEmpty(serverCode)) {
serverCode = "999";
}
String userid = serverCode + UIDUtil.next();
passidUser.setUserid(userid);
passidUser.setPassid(passid);
user.setPassid(passid);
user.setUserid(userid);
Date date = new Date();
user.setCreateTime(date);
user.setUpdateTime(date);
user.setDeleteFlag(0);
/*if(loggor.isInfoEnabled()){
loggor.info("passid:" + passid + " userid:" + userid + " msisdn:"
+ msisdn + " email:" + email);
}*/
int result = passidUserMapper.insert(passidUser);
if (passidUserMapper.insert(passidUser) > 0
&& userMapper.insertSelective(user) > 0)
redisService.set("passid:" + passid + ":userid", userid);
else
throw new SQLException("數(shù)據(jù)插入失敗,數(shù)據(jù)回滾");
return true;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/resources/conf/springConfig.xml")
public class RegisterUserServiceImplTest {
@InjectMocks
private RegisterUserService registerUserService = new RegisterUserServiceImpl();
@Mock
private UserMapper userMapper;
@Mock
private PassidUserMapper passidUserMapper;
@Mock
private CacheService redisService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(passidUserMapper.insert(any(PassidUser.class))).thenReturn(1);
when(userMapper.insertSelective(any(User.class))).thenReturn(1);
}
@Test
public void testInsert() throws Exception {
String passid = "12344";
String msisdn = "18867131210";
String email = "test@test.cn";
Assert.assertTrue(registerUserService.insert(passid, msisdn, email));
}
}
使用SpringMVC,Junit, Mockito測試Controller(Restful接口)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>${hamcrest.core.version}/version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Created by zhengcanrui on 16/8/11.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring/applicationContext-*xml"})
//配置事務(wù)的回滾,對數(shù)據(jù)庫的增刪改都會回滾,便于測試用例的循環(huán)利用
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional
@WebAppConfiguration
public class Test {
//記得配置log4j.properties ,的命令行輸出水平是debug
protected Log logger= LogFactory.getLog(TestBase.class);
protected MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@Before() //這個(gè)方法在每個(gè)方法執(zhí)行之前都會執(zhí)行一遍
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); //初始化MockMvc對象
}
@org.junit.Test
public void getAllCategoryTest() throws Exception {
String responseString = mockMvc.perform(
get("/categories/getAllCategory") //請求的url,請求的方法是get
.contentType(MediaType.APPLICATION_FORM_URLENCODED) //數(shù)據(jù)的格式
.param("pcode","root") //添加參數(shù)
).andExpect(status().isOk()) //返回的狀態(tài)是200
.andDo(print()) //打印出請求和相應(yīng)的內(nèi)容
.andReturn().getResponse().getContentAsString(); //將相應(yīng)的數(shù)據(jù)轉(zhuǎn)換為字符串
System.out.println("--------返回的json = " + responseString);
}
}
Spring MVC的測試往往看似比較復(fù)雜。其實(shí)他的不同在于,他需要一個(gè)ServletContext來模擬我們的請求和響應(yīng)。
@webappconfiguration是一級注釋,用于聲明一個(gè)ApplicationContext集成測試加載WebApplicationContext。作用是模擬ServletContext。
@ContextConfiguration:因?yàn)閏ontroller,component等都是使用注解,需要注解指定spring的配置文件,掃描相應(yīng)的配置,將類初始化等。
- perform:執(zhí)行一個(gè)RequestBuilder請求,會自動執(zhí)行SpringMVC的流程并映射到相應(yīng)的控制器執(zhí)行處理;
- get:聲明發(fā)送一個(gè)get請求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根據(jù)uri模板和uri變量值得到一個(gè)GET請求方式的。另外提供了其他的請求的方法,如:post、put、delete等。
- param:添加request的參數(shù),如上面發(fā)送請求的時(shí)候帶上了了pcode = root的參數(shù)。假如使用需要發(fā)送json數(shù)據(jù)格式的時(shí)將不能使用這種方式,可見后面被@ResponseBody注解參數(shù)的解決方法
- andExpect:添加ResultMatcher驗(yàn)證規(guī)則,驗(yàn)證控制器執(zhí)行完成后結(jié)果是否正確(對返回的數(shù)據(jù)進(jìn)行的判斷);
- andDo:添加ResultHandler結(jié)果處理器,比如調(diào)試時(shí)打印結(jié)果到控制臺(對返回的數(shù)據(jù)進(jìn)行的判斷);
- andReturn:最后返回相應(yīng)的MvcResult;然后進(jìn)行自定義驗(yàn)證/進(jìn)行下一步的異步處理(對返回的數(shù)據(jù)進(jìn)行的判斷);
寫RESTful接口測試時(shí)需要注意返回的數(shù)據(jù)格式要標(biāo)注成JSON : MediaType.APPLICATION_JSON
SoftInfo softInfo = new SoftInfo();
//設(shè)置值
ObjectMapper mapper = new ObjectMapper();
ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
java.lang.String requestJson = ow.writeValueAsString(softInfo);
String responseString = mockMvc.perform( post("/softs").contentType(MediaType.APPLICATION_JSON).content(requestJson)).andDo(print())
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
最后關(guān)于代碼覆蓋率
-
Intellij IDEA對這方面做了集成支持,右鍵運(yùn)行測試代碼
右鍵測試代碼選擇覆蓋率測試 -
可以在包含有單元測試的代碼中看到,綠色為已經(jīng)覆蓋的部分,紅色未覆蓋。
綠已覆蓋,紅未覆蓋

