Java單元測試淺析(JUnit+Mockito)

京東雲開發者 發佈 2024-05-07T02:54:51.977257+00:00

作者:京東物流 秦彪1. 什麼是單元測試(1)單元測試環節:測試過程按照階段劃分分為:單元測試、集成測試、系統測試、驗收測試等。相關含義如下:1) 單元測試: 針對電腦程式模塊進行輸出正確性檢驗工作。2) 集成測試: 在單元測試基礎上,整合各個模塊組成子系統,進行集成測試。

作者:京東物流 秦彪

1. 什麼是單元測試

(1)單元測試環節:

測試過程按照階段劃分分為:單元測試、集成測試、系統測試、驗收測試等。相關含義如下:

1) 單元測試: 針對電腦程式模塊進行輸出正確性檢驗工作。

2) 集成測試: 在單元測試基礎上,整合各個模塊組成子系統,進行集成測試。

3) 系統測試: 將整個交付所涉及的協作內容都納入其中考慮,包含計算機硬體、軟體、接口、操作等等一系列作為一個整體,檢驗是否滿足軟體或需求說明。

4) 驗收測試: 在交付或者發布之前對所做的工作進行測試檢驗。

單元測試是階段性測試的首要環節,也是白盒測試的一種,該內容的編寫與實踐可以前置在研發完成,研發在編寫業務代碼的時候就需要生成對應代碼的單元測試。單元測試的發起人是程序設計者,受益人也是編寫程序的人,所以對於程式設計師,非常有必要形成自我約束力,完成基本的單元測試用例編寫。

(2)單元測試特徵:

由上可知,單元測試其實是針對軟體中最小的測試單元來進行驗證的。這裡的單元就是指相關的功能子集,比如一個方法、一個類等。值得注意的是作為最低級別的測試活動,單元測試驗證的對象僅限於當前測試內容,與程序其它部分內容相隔離,總結起來單元測試有以下特徵:

1) 主要功能是證明編寫的代碼內容與期望輸出一致。

2) 最小最低級的測試內容,由程式設計師自身發起,保證程序基本組件正常。

3) 單元測試儘量不要區分類與方法,主張以過程性的方法為測試單位,簡單實用高效為目標。

4) 不要偏離主題,專注於測試一小塊的代碼,保證基礎功能。

5) 剝離與外部接口、存儲之間的依賴,使單元測試可控。

6) 任何時間任何順序執行單元測試都需要是成功的。

2. 為什麼要單元測試

(1)單元測試意義:

程序代碼都是由基本單元不斷組合成複雜的系統,底層基本單元都無法保證輸入輸出正確性,層級遞增時,問題就會不斷放大,直到整個系統崩潰無法使用。所以單元測試的意義就在於保證基本功能是正常可用且穩定的。而對於接口、數據源等原因造成的不穩定因素,是外在原因,不在單元測試考慮範圍之內。

(2)使用main方法進行測試:

@PostMapping(value="/save")
public Map<String,Object> save(@RequestBody Student stu) {
    studentService.save(stu);
    Map<String,Object> params = new HashMap<>();
    params.put("code",200);
    params.put("message","保存成功");
    return params;
}

假如要對上面的Controller進行測試,可以編寫如下的代碼示例,使用main方法進行測試的時候,先啟動整個工程應用,然後編寫main方法如下進行訪問,在單步調試代碼。

public static void main(String[] args) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        String json = "{\"name\":\"張三\",\"className\":\"三年級一班\",\"age\":\"20\",\"sex\":\"男\"}";
        HttpEntity<String> httpEntity = new HttpEntity<>(json, headers);
        String url = "http://localhost:9092/student/save";
        MainMethodTest test = new MainMethodTest();
        ResponseEntity<Map> responseEntity = test.getRestTemplate().postForEntity(url, httpEntity, Map.class);
        System.out.println(responseEntity.getBody());
    }

(3)使用main方法進行測試的缺點:

1) 通過編寫大量的main方法針對每個內容做列印輸出到控制台枯燥繁瑣,不具備優雅性。

2) 測試方法不能一起運行,結果需要程式設計師自己判斷正確性。

3) 統一且重複性工作應該交給工具去完成。

3. 單元測試框架-Junit

3.1 junit簡介

JUnit官網:https://junit.org/。JUnit是一個用於編寫可重複測試的簡單框架。它是用於單元測試框架的xUnit體系結構的一個實例。

JUnit的特點:

(1) 針對於Java語言特定設計的單元測試框架,使用非常廣泛。

(2) 特定領域的標準測試框架。

(3) 能夠在多種IDE開發平台使用,包含Idea、Eclipse中進行集成。

(4) 能夠方便由Maven引入使用。

(5) 可以方便的編寫單元測試代碼,查看測試結果等。

JUnit的重要概念:

名稱

功能作用

Assert

斷言方法集合

TestCase

表示一個測試案例

TestSuite

包含一組TestCase,構成一組測試

TestResult

收集測試結果

JUnit的一些注意事項及規範:

(1) 測試方法必須使用@Test 修飾

(2) 測試方法必須使用public void 進行修飾,不能帶參數

(3) 測試代碼的包應該和被測試代碼包結構保持一致

(4) 測試單元中的每個方法必須可以獨立測試,方法間不能有任何依賴

(5) 測試類一般使用 Test作為類名的後綴

(6) 測試方法使一般用test 作為方法名的前綴

JUnit失敗結果說明:

(1) Failure:測試結果和預期結果不一致導致,表示測試不通過

(2) error:由異常代碼引起,它可以產生於測試代碼本身的錯誤,也可以是被測代碼的Bug

3.2 JUnit內容

(1) 斷言的API

斷言方法

斷言描述

assertNull(String message, Object object)

檢查對象是否為空,不為空報錯

assertNotNull(String message, Object object)

檢查對象是否不為空,為空報錯

assertEquals(String message, Object expected, Object actual)

檢查對象值是否相等,不相等報錯

assertTrue(String message, boolean condition)

檢查條件是否為真,不為真報錯

assertFalse(String message, boolean condition)

檢查條件是否為假,為真報錯

assertSame(String message, Object expected, Object actual)

檢查對象引用是否相等,不相等報錯

assertNotSame(String message, Object unexpected, Object actual)

檢查對象引用是否不等,相等報錯

assertArrayEquals(String message, Object[] expecteds, Object[] actuals)

檢查數組值是否相等,遍歷比較,不相等報錯

assertArrayEquals(String message, Object[] expecteds, Object[] actuals)

檢查數組值是否相等,遍歷比較,不相等報錯

assertThat(String reason, T actual, Matcher<? super T> matcher)

檢查對象是否滿足給定規則,不滿足報錯

(2) JUnit常用註解:

1) @Test: 定義一個測試方法 @Test(excepted=xx.class): xx.class 表示異常類,表示測試的方法拋出此異常時,認為是正常的測試通過的 @Test(timeout = 毫秒數) :測試方法執行時間是否符合預期。

2) @BeforeClass: 在所有的方法執行前被執行,static 方法全局只會執行一次,而且第一個運行。

3) @AfterClass:在所有的方法執行之後進行執行,static 方法全局只會執行一次,最後一個運行。

4) @Before:在每一個測試方法被運行前執行一次。

5) @After:在每一個測試方法運行後被執行一次。

6) @Ignore:所修飾的測試方法會被測試運行器忽略。

7) @RunWith:可以更改測試執行器使用junit測試執行器。

3.3 JUnit使用

3.3.1 Controller層單元測試

(1) Springboot中使用maven引入Junit非常簡單, 使用如下依賴即可引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>Spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

(2) 上面使用main方法案例可以使用如下的Junit代碼完成:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class StudentControllerTest {
        
        // 注入Spring容器
    @Autowired
    private WebApplicationContext applicationContext;
    // 模擬Http請求
    private MockMvc mockMvc;

    @Before
    public void setupMockMvc(){
        // 初始化MockMvc對象
        mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
    }
    
    /**
     * 新增學生測試用例
     * @throws Exception
     */
    @Test
    public void addStudent() throws Exception{
        String json="{\"name\":\"張三\",\"className\":\"三年級一班\",\"age\":\"20\",\"sex\":\"男\"}";
        mockMvc.perform(MockMvcRequestBuilders.post("/student/save")    //構造一個post請求
                    // 發送端和接收端數據格式
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8)
                    .content(json.getBytes())
            )
           // 斷言校驗返回的code編碼
           .andExpect(MockMvcResultMatchers.status().isOk())
           // 添加處理器列印返回結果
           .anddo(MockMvcResultHandlers.print());
    }
}

只需要在類或者指定方法上右鍵執行即可,可以直接充當postman工作訪問指定url,且不需要寫請求代碼,這些都由工具自動完成。


(3)案例中相關組件介紹

本案例中構造mockMVC對象時,也可以使用如下方式:

@Autowired
private StudentController studentController;
@Before
public void setupmockMvc(){
   // 初始化MockMvc對象
   mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();
}

其中MockMVC是Spring測試框架提供的用於REST請求的工具,是對Http請求的模擬,無需啟動整個模塊就可以對Controller層進行調用,速度快且不依賴網絡環境。

使用MockMVC的基本步驟如下:

1) mockMvc.perform執行請求

2) MockMvcRequestBuilders.post或get構造請求

3) MockHttpServletRequestBuilder.param或content添加請求參數

4) MockMvcRequestBuilders.contentType添加請求類型

5) MockMvcRequestBuilders.accept添加響應類型

6) ResultActions.andExpect添加結果斷言

7) ResultActions.andDo添加返回結果後置處理

8) ResultActions.andReturn執行完成後返回相應結果

3.3.2 Service層單元測試

可以編寫如下代碼對Service層查詢方法進行單測:

@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {

        @Autowired
    private StudentService studentService;

    @Test
    public void getOne() throws Exception {
         Student stu = studentService.selectByKey(5);
         Assert.assertThat(stu.getName(),CoreMatchers.is("張三"));
    }
}

執行結果:

3.3.3 Dao層單元測試

可以編寫如下代碼對Dao層保存方法進行單測:

@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentDaoTest {

        @Autowired
    private StudentMapper studentMapper;

    @Test
    @Rollback(value = true)
    @Transactional
    public void insertOne() throws Exception {
         Student student = new Student();
         student.setName("李四");
         student.setMajor("計算機學院");
         student.setAge(25);
         student.setSex('男');
         int count = studentMapper.insert(student);
         Assert.assertEquals(1, count);
    }
}

其中@Rollback(value = true) 可以執行單元測試之後回滾所新增的數據,保持資料庫不產生髒數據。

3.3.4 異常測試

(1) 在service層定義一個異常情況:

public void computeScore() {
   int a = 10, b = 0;
}

(2) 在service的測試類中定義單元測試方法:

@Test(expected = ArithmeticException.class)
    public void computeScoreTest() {
        studentService.computeScore();
    }

(3) 執行單元測試也會通過,原因是@Test註解中的定義了異常

3.3.5 測試套件測多個類

(1) 新建一個空的單元測試類

(2) 利用註解@RunWith(Suite.class)和@SuiteClasses標明要一起單元測試的類

@RunWith(Suite.class)
@Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class})
public class AllTest {
}

運行結果:

3.3.6 idea中查看單元測試覆蓋率

(1) 單測覆蓋率

測試覆蓋率是衡量測試過程工作本身的有效性,提升測試效率和減少程序bug,提升產品可靠性與穩定性的指標。

統計單元測試覆蓋率的意義:

1) 可以洞察整個代碼中的基礎組件功能的所有盲點,發現相關問題。

2) 提高代碼質量,通常覆蓋率低表示代碼質量也不會太高,因為單測不通過本來就映射出考慮到各種情況不夠充分。

3) 從覆蓋率的達標上可以提高代碼的設計能力。

(2) 在idea中查看單元測試覆蓋率很簡單,只需按照圖中示例的圖標運行,或者在單元測試方法或類上右鍵Run 'xxx' with Coverage即可。執行結果是一個表格,列出了類、方法、行數、分支覆蓋情況。

(3) 在代碼中會標識出覆蓋情況,綠色的是已覆蓋的,紅色的是未覆蓋的。

(4) 如果想要導出單元測試的覆蓋率結果,可以使用如下圖所示的方式,勾選 Open generated HTML in browser

導出結果:

3.3.7 JUnit插件自動生成單測代碼

(1) 安裝插件,重啟idea生效

(2) 配置插件


(3) 使用插件

在需要生成單測代碼的類上右鍵generate...,如下圖所示。

生成結果:


4. 單元測試工具-Mockito

4.1 Mockito簡介

在單元測試過程中主張不要依賴特定的接口與數據來源,此時就涉及到對相關數據的模擬,比如Http和JDBC的返回結果等,可以使用虛擬對象即Mock對象進行模擬,使得單元測試不在耦合。

Mock過程的使用前提:

(1) 實際對象時很難被構造出來的

(2) 實際對象的特定行為很難被觸發

(3) 實際對象可能當前還不存在,比如依賴的接口還沒有開發完成等等。

Mockito官網:https://site.mockito.org 。Mockito和JUnit一樣是專門針對Java語言的mock數據框架,它與同類的EasyMock和jMock功能非常相似,但是該工具更加簡單易用。

Mockito的特點:

(1) 可以模擬類不僅僅是接口

(2) 通過註解方式簡單易懂

(3) 支持順序驗證

(4) 具備參數匹配器

4.2 Mockito使用

maven引入spring-boot-starter-test會自動將mockito引入到工程中。

4.2.1 使用案例

(1) 在之前的代碼中在定義一個BookService接口, 含義是借書接口,暫且不做實現

public interface BookService {
    Book orderBook(String name);
}

(2) 在之前的StudentService類中新增一個orderBook方法,含義是學生預定書籍方法,其中實現內容調用上述的BookService的orderBook方法。

public Book orderBook(String name) {
   return bookService.orderBook(name);
}

(3) 編寫單元測試方法,測試StudentService的orderBook方法

@Test
public void orderBookTest() {
    Book expectBook = new Book(1L, "鋼鐵是怎樣煉成的", "書架A01");
    Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);
    Book book = studentService.orderBook("");
    System.out.println(book);
    Assert.assertTrue("預定書籍不符", expectBook.equals(book));
}

(4) 執行結果:

(5) 結果解析

上述內容並沒有實現BookService接口的orderBook(String name)方法。但是使用mockito進行模擬數據之後,卻通過了單元測試,原因就在於Mockito替換了本來要在StudentService的orderBook方法中獲取的對象,此處就模擬了該對象很難獲取或當前無法獲取到,用模擬數據進行替代。


4.2.2 相關語法

常用API:

上述案例中用到了mockito的when、any、theWhen等語法。接下來介紹下都有哪些常用的API:

1) mock:模擬一個需要的對象

2) when:一般配合thenXXX一起使用,表示當執行什麼操作之後怎樣。

3) any: 返回一個特定對象的預設值,上例中標識可以填寫任何String類型的數據。

4) theReturn: 在執行特定操作後返回指定結果。

5) spy:創造一個監控對象。

6) verify:驗證特定的行為。

7) doReturn:返回結果。

8) doThrow:拋出特定異常。

9) doAnswer:做一個自定義響應。

10) times:操作執行次數。

11) atLeastOnce:操作至少要執行一次。

12) atLeast:操作至少執行指定的次數。

13) atMost:操作至多執行指定的次數。

14) atMostOnce:操作至多執行一次。

15) doNothing:不做任何的處理。

16) doReturn:返回一個結果。

17) doThrow:拋出一個指定異常。

18) doAnswer:指定一個特定操作。

19) doCallRealMethod:用於監控對象返回一個真實結果。

4.2.3 使用要點

(1) 打樁

Mockito中有Stub,所謂存根或者叫打樁的概念,上面案例中的Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);就是打樁的含義,先定義好如果按照既定的方式調用了什麼,結果就輸出什麼。然後在使用Book book = studentService.orderBook(""); 即按照指定存根輸出指定結果。

    @Test
    public void verifyTest() {
        List mockedList = mock(List.class);

        mockedList.add("one");

        verify(mockedList).add("one");                  // 驗證通過,因為前面定義了這個樁
        verify(mockedList).add("two");                  // 驗證失敗,因為前面沒有定義了這個樁
    }

(2) 參數匹配

上例StudentService的orderBook方法中的any(String.class) 即為參數匹配器,可以匹配任何此處定義的String類型的數據。

(3) 次數驗證

    @Test
    public void timesTest() {
        List mockedList = mock(List.class);
        when(mockedList.get(anyInt())).thenReturn(1000);
        System.out.println(mockedList.get(1));
        System.out.println(mockedList.get(1));
        System.out.println(mockedList.get(1));
        System.out.println(mockedList.get(2));

        // 驗證通過:get(1)被調用3次
        verify(mockedList, times(3)).get(1);
        // 驗證通過:get(1)至少被調用1次
        verify(mockedList, atLeastOnce()).get(1);
        // 驗證通過:get(1)至少被調用3次
        verify(mockedList, atLeast(3)).get(1);
    }

(4) 順序驗證

    @Test
    public void orderBookTest1() {
            String json = "{\"id\":12,\"location\":\"書架A12\",\"name\":\"三國演義\"}";
            String json1 = "{\"id\":21,\"location\":\"書架A21\",\"name\":\"水滸傳\"}";
        String json2 = "{\"id\":22,\"location\":\"書架A22\",\"name\":\"紅樓夢\"}";
        String json3 = "{\"id\":23,\"location\":\"書架A23\",\"name\":\"西遊記\"}";
        when(bookService.orderBook("")).thenReturn(JSON.parseObject(json, Book.class));
        Book book = bookService.orderBook("");
        Assert.assertTrue("預定書籍有誤", "三國演義".equals(book.getName()));

        when(bookService.orderBook("")).thenReturn(JSON.parseObject(json1, Book.class)).
                thenReturn(JSON.parseObject(json2, Book.class)).
                thenReturn(JSON.parseObject(json3, Book.class));
        Book book1 = bookService.orderBook("");
        Book book2 = bookService.orderBook("");
        Book book3 = bookService.orderBook("");
        Book book4 = bookService.orderBook("");
        Book book5 = bookService.orderBook("");
        // 全部驗證通過,按順序最後打樁打了3次,大於3次按照最後對象輸出
        Assert.assertTrue("預定書籍有誤", "水滸傳".equals(book1.getName()));
        Assert.assertTrue("預定書籍有誤", "紅樓夢".equals(book2.getName()));
        Assert.assertTrue("預定書籍有誤", "西遊記".equals(book3.getName()));
        Assert.assertTrue("預定書籍有誤", "西遊記".equals(book4.getName()));
        Assert.assertTrue("預定書籍有誤", "西遊記".equals(book5.getName()));
}

(5) 異常驗證

    @Test(expected = RuntimeException.class)
    public void exceptionTest() {
        List mockedList = mock(List.class);
        doThrow(new RuntimeException()).when(mockedList).add(1);
        // 驗證通過
        mockedList.add(1);
    }
關鍵字: