spring-boog-測試打樁-Mockito

Mockito用於測試時進行打樁處理;經過它能夠指定某個類的某個方法在什麼狀況下返回什麼樣的值。

例如:測試 controller時,依賴 service,這個時候就能夠假設當調用 service 某個方法時返回指定的某些值,從而來下降引用類所帶來的測試複雜度增長的影響。Mockito就用於這種場景。java

Mockito經常使用測試場景描述以下:

  • 指定打樁對象的返回值
  • 判斷某個打樁對象的某個方法被調用及調用的次數
  • 指定打樁對象拋出某個特定異常

Mockito的使用,通常有如下幾種組合:web

  • do/when:包括doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)
  • given/will:包括given(…).willReturn(…)/given(…).willAnswer(…)
  • when/then: 包括when(…).thenReturn(…)/when(…).thenAnswer(…)

指定打樁對象返回值

經過Mockito指定打樁對象的返回值時,能夠經過如下方式進行:spring

given

given用於對指定方法進行返回值的定製,它須要與will開頭的方法一塊兒使用,will開頭的方式根據其接收參數的不一樣,又分紅兩類:一是接收直接值的,如直接指定返回結果爲某個常量;二是接收Answer參數的,能夠騎過Answer類的answer方法來根據傳入參數定製返回結果。json

Answer對象

咱們實際針對的通常是某個類的某個方法;這個方法可能會有輸入參數;考慮這種場景:若是要假設打樁的這個方法,在某個輸入時返回值A;在另一個輸入時返回值爲B;這種場景就能夠經過Answer類來實現。api

given + willAnswer/will

案例 根據傳入的參數,返回不一樣的數據springboot

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

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mvc;

    private MockHttpSession session;


    /**
     * 1. 對於不須要返回的任何值的類的全部方法,能夠直接使用MockBean
     * 2. @MockBean 會代理已有的bean的方法,不會執行真實 bean 的具體方法。
     */
    @MockBean
    private LearnService learnService;

    @Before
    public void setupMockMvc() {
        //初始化MockMvc對象
        mvc = MockMvcBuilders.webAppContextSetup(wac).build();

        //構建session
        session = new MockHttpSession();
        User user = new User("root", "root");
        //攔截器那邊會判斷用戶是否登陸,因此這裏注入一個用戶
        session.setAttribute("user", user);
    }


    /**
     * 獲取教程測試用例
     * <p>
     * get 請求
     * <p>
     * controller 依賴 service 的方法,這裏給 service 方法打樁,不執行真實的方法
     *
     * @throws Exception
     */
    @Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        // 當調用 selectByKey 函數時,返回指定的值
        given(this.learnService.selectByKey(Mockito.any())).willAnswer(new Answer<Object>() {

            /**
             * InvocationOnMock 經過它能夠獲取打樁方法的實際傳入參數清單
             * @param invocationOnMock
             * @return
             * @throws Throwable
             */

            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Long argumentAt = invocationOnMock.getArgumentAt(0, Long.class);
                System.out.println("調用方法的實際參數: " + argumentAt);
                if (argumentAt.equals(Long.parseLong("1001"))) {
                    return learnResource;
                }
                return null;
            }
        });

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }


}

given + willReturn

經過willReturn能夠直接指定打樁的方法的返回值
案例 在任何場景下,都返回指定的數據session

/**
     * 獲取教程測試用例
     * <p>
     * get 請求
     * <p>
     * controller 依賴 service 的方法,這裏給 service 方法打樁,不執行真實的方法
     *
     * @throws Exception
     */
    @Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);
        
        given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }

異常信息:
java.lang.AssertionError: JSON path "$.author" 
Expected :嘟嘟MD獨立博客
Actual   :zhang
 <Click to see difference>

when + thenReturn

thenReturn與willReturn相似mvc

/**
     * 獲取教程測試用例
     * <p>
     * get 請求
     * <p>
     * controller 依賴 service 的方法,這裏給 service 方法打樁,不執行真實的方法
     *
     * @throws Exception
     */
    @Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        when(this.learnService.selectByKey(Mockito.any())).thenReturn(learnResource);

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }

when + thenAnswer/then

thenAnswer與willAnswer也相似ide

/**
     * 獲取教程測試用例
     * <p>
     * get 請求
     * <p>
     * controller 依賴 service 的方法,這裏給 service 方法打樁,不執行真實的方法
     *
     * @throws Exception
     */
    @Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        when(this.learnService.selectByKey(Mockito.any())).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Long argumentAt = invocationOnMock.getArgumentAt(0, Long.class);
                System.out.println("調用方法的實際參數: " + argumentAt);
                if (argumentAt.equals(Long.parseLong("1001"))) {
                    return learnResource;
                } else if (argumentAt.equals(Long.parseLong("1002"))) {
                    learnResource.setAuthor("keke");
                    return learnResource;
                }
                return null;
            }
        });

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1002")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }

異常:
參數爲 1001 時
java.lang.AssertionError: JSON path "$.author" 
Expected :嘟嘟MD獨立博客
Actual   :zhang
 <Click to see difference>

參數爲 1002 時
java.lang.AssertionError: JSON path "$.author" 
Expected :嘟嘟MD獨立博客
Actual   :keke
 <Click to see difference>

doAnswer/doReturn + when

// mock 對象不能是 @MockBean 生成的,@MockBean請況下不能用
@Test
    public void testAnswer1() {
        List<String> mock = Mockito.mock(List.class);
        Mockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Object[] args = invocationOnMock.getArguments();
                System.out.println(args[0]);
                Integer num = (Integer) args[0];
                if (num > 3) {
                    return "大於三";
                } else {
                    return "小於三";
                }
            }
        }).when(mock).get(Mockito.anyInt());
        // 當 索引爲 4 時,指望 大於三
       Assert.assertThat(mock.get(4), equalTo("大於三"));
        // 當 索引爲 2 時,指望 小於三
       Assert.assertThat(mock.get(4), equalTo("小於三"));
    }

// mock 對象不能是 @MockBean 生成的,@MockBean請況下不能用
   @Test
    public void testAnswer1() {
        List<String> mock = Mockito.mock(List.class);
        Mockito.doReturn("大於三").when(mock).get(Mockito.anyInt());
        // 當 索引爲 2 時
        Assert.assertThat(mock.get(2), equalTo("大於三"));

    }

判斷某個打樁對象的某個方法被調用及調用的次數

@Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());


        // 判斷 learnService.selectByKey 方法 是否調用了
        Mockito.verify(learnService).selectByKey(1001L);

        // 判斷 learnService.selectByKey 方法,指望調用 2 次,能過 times 函數指定 selectByKey 函數指望調用幾回
        // 也能夠經過 Mockito.atLeast 最少幾回,Mockito.atMost 是多幾回 等函數判斷
        Mockito.verify(learnService, Mockito.times(2)).selectByKey(1001L);

    }

異常:由於 learnService.selectByKey 方法,調用了1次,而指望調用兩次,因此測試出錯
org.mockito.exceptions.verification.TooLittleActualInvocations: 
learnServiceImpl bean.selectByKey(1001);
Wanted 2 times:
-> at com.dudu.outher.LearnController7Test.qryLearn(LearnController7Test.java:86)
But was 1 time:
-> at com.dudu.controller.LearnController.qryLearn(LearnController.java:88)

指定打樁對象拋出某個特定異常

given+willThrow函數

@Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        // 調用 learnService.selectByKey 方法時,拋出異常
        given(this.learnService.selectByKey(Mockito.any())).willThrow(new Exception("查詢出錯"));

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());


    }

異常:
org.mockito.exceptions.base.MockitoException: 
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯

when+thenThrow

@Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("zhang");
        learnResource.setAuthor("zhang");
        learnResource.setId(10L);

        when(this.learnService.selectByKey(Mockito.any())).thenThrow(new Exception("查詢出錯"));

        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }

異常:
org.mockito.exceptions.base.MockitoException: 
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯

doThrow+when
不能用於 @MockBean 場景下

@Test
    public void testAnswer1() {
        List<String> mock = Mockito.mock(List.class);

        // 調用 mock.size 時,拋出指望的異常信息
        Mockito.doThrow(new Exception("查詢出錯")).when(mock).size();

        // 調用 mock 對象的方法
        mock.size();

    }

異常:
org.mockito.exceptions.base.MockitoException: 
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯

參考

doThrow:在模擬對象中調用方法時想要拋出異常時使用.
doReturn:在執行方法時要返回返回值時使用.
doAnswer:須要對傳遞給方法的參數執行一些操做
doNothing:是最簡單的列表,基本上它告訴Mockito在調用模擬對象中的方法時什麼也不作.有時用於void返回方法或沒有反作用的方法,或者與您正在進行的單元測試無關

https://blog.csdn.net/icarusliu/article/details/78860351

靜態方法測試

Mockito沒法對靜態方法進行Mock,若是須要Mock靜態方法,須要使用到PowerMockito

<dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito</artifactId>
            <version>1.7.1</version>
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>1.7.1</version>
        </dependency>

單元測試時,須要使用PowerMockRunner及PrepareForTest兩個註解

@RunWith(PowerMockRunner.class)
// 對 StringUtils 靜態方法進行測試
@PrepareForTest({StringUtils.class})
public class TestStatic {

  @Test
    public void testStaticMethod() {
        // 對 StringUtils 打樁
        PowerMockito.mockStatic(StringUtils.class);
        PowerMockito.when(StringUtils.isNoneBlank(Mockito.anyString())).thenReturn(false);
        boolean bbb = StringUtils.isNoneBlank("bbb");
        System.out.println(bbb);
    }
}

與SpringBootTest一塊兒使用

SpringBootTest必需要使用SpringRunner才能生效;但RunWith沒有辦法指定多個,能夠經過PowerMockRunnerDelegate來解決這個問題:

@RunWith(PowerMockRunner.class)//使用powermock提供的代理來使用
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})//忽略一些powermock使用的classloader沒法處理的類
@PrepareForTest({StringUtils.class})// @PrepareForTest 能夠 mock 多個靜態方法
@SpringBootTest
public class LearnController11Test {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mvc;

    private MockHttpSession session;


    @MockBean
    private LearnService learnService;

    @Before
    public void setupMockMvc() {
        //初始化MockMvc對象
        mvc = MockMvcBuilders.webAppContextSetup(wac).build();

        //構建session
        session = new MockHttpSession();
        User user = new User("root", "root");
        //攔截器那邊會判斷用戶是否登陸,因此這裏注入一個用戶
        session.setAttribute("user", user);
    }


    /**
     * 獲取教程測試用例
     * <p>
     * get 請求
     * <p>
     * controller 依賴 service 的方法,這裏給 service 方法打樁,不執行真實的方法
     *
     * @throws Exception
     */
    @Test
    public void qryLearn() throws Exception {

        LearnResource learnResource = new LearnResource();
        learnResource.setUrl("http://www.baidu.com");
        learnResource.setTitle("Spring Boot乾貨系列");
        learnResource.setAuthor("嘟嘟MD獨立博客");
        learnResource.setId(10L);

        // 對 service層中的方法進行 mock
        given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);

        // 對 StringUtils 打樁,mock 靜態方法
        PowerMockito.mockStatic(StringUtils.class);
        // 當 執行 StringUtils.isNoneBlank 方法時,返回 false
        PowerMockito.when(StringUtils.isNoneBlank(Mockito.anyString())).thenReturn(false);

        // 實際使用中 StringUtils.isNoneBlank("bbb") 返回 true,但這裏返回 false
        boolean result = StringUtils.isNoneBlank("bbb");
        System.out.println("StringUtils.isNoneBlank: " + result);


        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                //jsonPath用來獲取author字段比對是否爲嘟嘟MD獨立博客,不是就測試不經過
                .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot乾貨系列"))
                .andDo(MockMvcResultHandlers.print());
    }


}
相關文章
相關標籤/搜索