Java測試套件 - Junit與Mockito

說明

? ? ? ? 在第一份工作中,我經(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)充。

碧海之藍(lá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

Junit 4 官網(wǎng)的用例

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)類。

深入JUnit源碼之Rule類圖 - 來源于博文《深入JUnit源碼之Rule》
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)用的依賴的行為。

對象間存在依賴關(guān)系
使用Mock來模仿對象,消除耦合關(guān)系

下面的代碼來自官網(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)覆蓋的部分,紅色未覆蓋。


    綠已覆蓋,紅未覆蓋

Junit 5 未完待續(xù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容