在日常的springboot專案開發中,總會需要寫一些單元測驗用例,一些單元測驗的方法用的比較少,撰寫時又需要去查詢,因此在此總結一些測驗案例
Junit是目前主流的單元測驗框架,我,常用的為Junit4,以下測驗案例是基于Junit4來撰寫
單元測驗的目的與好處
1、單元測驗能有效地幫你發現代碼中的 bug
單元測驗往往需要走通方法中的各條路徑,通過單元測驗常常會發現代碼中的很多考慮不全面的地方
2、寫單元測驗能幫你發現代碼設計上的問題
對于一段代碼,如果很難為其撰寫單元測驗,或者單元測驗寫起來很吃力,需要依靠單元測驗框架里很高級的特性才能完成,那往往就意味著代碼設計得不夠合理
3、單元測驗是對集成測驗的有力補充
對于一些復雜系統來說,集成測驗也無法覆寫得很全面,復雜系統往往有很多模塊,每個模塊都有各種輸入、輸出、例外情況,組合起來,整個系統就有無數測驗場景需要模擬,無數的測驗用例需要設計,再強大的測驗團隊也無法窮舉完備
4、寫單元測驗的程序本身就是代碼重構的程序
設計和實作代碼的時候,我們很難把所有的問題都想清楚,而撰寫單元測驗就相當于對代碼的一次自我 Code Review,在這個程序中,我們可以發現一些設計上的問題(比如代碼設計的不可測驗)以及代碼撰寫方面的問題(比如一些邊界條件處理不當)等,然后針對性的進行重構,
所需依賴
<!-- 使用MockMvc發起請求時需要該依賴, Spring Boot 2.2.0版本開始引入 JUnit5 作為單元測驗默認庫 JUnit5和JUnit4之間會有沖突,這里屏蔽掉JUnit5--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <exclusions> <exclusion> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> </exclusion> <exclusion> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> </exclusion> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> <scope>test</scope> </dependency> <!-- junit4所需測驗依賴 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <!-- 使用powermock所需依賴 --> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.12.4</version> <scope>test</scope> </dependency>
常見注解
@Before:初始化方法,在任何一個測驗方法執行之前,必須執行的代碼
@BeforeClass:針對所有測驗,也就是整個測驗類中,在所有測驗方法執行前,都會先執行由它注解的方法,而且只執行一次,修飾符必須是 public static void
@After:釋放資源,在任何一個測驗方法執行之后,需要進行的收尾作業
@AfterClass:針對所有測驗,也就是整個測驗類中,在所有測驗方法都執行完之后,才會執行由它注解的方法,而且只執行一次,修飾符必須是 public static void
@Test:測驗方法,表明這是一個測驗方法,在 JUnit 中將會自動被執行,對與方法的宣告也有如下要求:名字可以隨便取,沒有任何限制,但是回傳值必須為 void ,而且不能有任何引數
@RunWith(MockitoJUnitRunner.class) public class AnnotationTest { public static final Logger log = LoggerFactory.getLogger(AnnotationTest.class); @Before public void init(){ log.info("@Before call"); } @BeforeClass public static void beforeClass(){ log.info("@BeforeClass call"); } @After public void after(){ log.info("@After call"); } @AfterClass public static void afterClass(){ log.info("@AfterClass call"); } @Test public void test01(){ log.info("test01 call"); } @Test public void test02(){ log.info("test02 call"); } }
方法執行結果如下所示,兩個測驗方法,@Before和@After都執行了兩次,而@BeforeClass和@AfterClass都只執行了一次;執行順序:@BeforeClass --> @Before --> @Test --> @After --> @AfterClass

斷言
Junit提供的斷言主要有如下幾種型別:
Assert.assertTrue():驗證條件是否為真
Assert.assertFalse():驗證條件是否為假
Assert.assertEquals():驗證兩個值是否相等
Assert.assertNotNull():驗證物件是否為空
Assert.assertThrows():驗證執行代碼是否拋出了指定型別的例外
verify:
@Test
public void test(){
List list = Mockito.mock(List.class);
list.add("a");
//Mockito.times()不寫默認指呼叫1次
Mockito.verify(list).add("a");
Mockito.verify(list,Mockito.times(1)).add("a");
//判讀list.add方法被呼叫2次
list.add("a");
Mockito.verify(list,Mockito.times(2)).add("a");
}
@Mock與@InjectMocks的區別
@Mock: 創建一個Mock.
@InjectMocks: 創建一個實體,簡單的說是這個Mock可以呼叫真實代碼的方法,其余用@Mock注解創建的mock將被注入到用該實體中,
Mockito的初始化
當我們要使用注解(比如@Mock)來mock物件的使用,就要初始化Mockito,這樣用@Mock標注的物件才會被實體化,否則直接使用會報Null指標例外,其有兩種初始化的方法:
1、使用MockitoAnnotations.initMocks方法
public class InitMockA { public static final Logger log = LoggerFactory.getLogger(InitMockA.class); @Before public void init(){ MockitoAnnotations.initMocks(this); } @Test public void test01(){ log.info("run test01"); } }
2、類上使用@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.class) public class InitMockB { public static final Logger log = LoggerFactory.getLogger(InitMockB.class); @Test public void test01(){ log.info("run test01"); } }
案例
1、常見簡單測驗案例
@Service public class AComponent { @Value("${test-case.key}") private String key; @Autowired private UserInfoMapper userInfoMapper; @Autowired private BComponent bComponent; public UserInfo normalMethod(Integer id){ UserInfo userInfo = userInfoMapper.getById(id); System.out.println(userInfo.getSex()); return userInfo; } public boolean compareUser(Integer originId,Integer targetId){ UserInfo originUser = userInfoMapper.getById(originId); UserInfo targetUser = userInfoMapper.getById(targetId); return originUser.getSex().equals(targetUser.getSex()); } public void complicatedService(ServiceEntity serviceEntity, String name){ //... bComponent.complicatedMethod(serviceEntity,name); //... } public UserInfo exceptionService(Integer id){ UserInfo userInfo = null; try { userInfo = bComponent.exceptionMethod(id); }catch (Exception e){ return null; } return userInfo; } public void updateUserInfo(UserInfo userInfo){ userInfoMapper.updateUserInfo(userInfo); } public String getKey(){ return key; } }
測驗方法:
@RunWith(MockitoJUnitRunner.class) public class SimpleTest { @Mock private UserInfoMapper userInfoMapper; @Mock private BComponent bComponent; /** * 創建一個實體,簡單的說是這個Mock可以呼叫真實代碼的方法,其余用@Mock注解創建的mock將被注入到用該實體中 */ @InjectMocks AComponent aComponent; @Before public void intit(){ // 為aComponent注入物件 ReflectionTestUtils.setField(aComponent,"key","abcdefg"); } /** * 最常見的測驗用例,mock回傳值 */ @Test public void normalTest(){ Integer id = 1; Mockito.when(userInfoMapper.getById(id)).thenReturn(getManUserInfo()); aComponent.normalMethod(id); Mockito.verify(userInfoMapper).getById(id); } /** * 測驗同一個方法,入參不同的,回傳值也不相同 */ @Test public void differentParamTest(){ Integer user1 = 1; Integer user2 = 2; Mockito.when(userInfoMapper.getById(user1)).thenReturn(getManUserInfo()); Mockito.when(userInfoMapper.getById(user2)).thenReturn(getFemaleUserInfo()); boolean result = aComponent.compareUser(user1,user2); Assert.assertFalse(result); } /** * 入參比較復雜的時候可以使用Mockito.any,入參也是可以mock的 * Mockito.any()可以有多種型別,比如: * Mockito.any(ServiceEntity.class); * Mockito.anyString(); * Mockito.anyCollection(); * Mockito.anyList(); */ @Test public void paramComplicated(){ aComponent.complicatedService(Mockito.any(),Mockito.anyString()); Mockito.verify(bComponent).complicatedMethod(Mockito.any(),Mockito.anyString()); } /** * 當方法中出現例外的時候,可以使用doThrow方法自己制造例外 */ @Test public void exceptionTest(){ Integer id = 1; Mockito.doThrow(new IllegalArgumentException()).when(bComponent).exceptionMethod(id); UserInfo userInfo = aComponent.exceptionService(id); Assert.assertTrue(userInfo == null); } @Test public void keyTest(){ String key = aComponent.getKey(); Assert.assertTrue("abcdefg".endsWith(key)); } private UserInfo getManUserInfo(){ UserInfo userInfo = new UserInfo(); userInfo.setId(1); userInfo.setUserName("zhansan"); userInfo.setAge(12); userInfo.setSex("M"); return userInfo; } private UserInfo getFemaleUserInfo(){ UserInfo userInfo = new UserInfo(); userInfo.setId(2); userInfo.setUserName("李四"); userInfo.setAge(12); userInfo.setSex("F"); return userInfo; } }
2、Mock靜態方法:
@Service public class StaticComponent { /** * 這里為是shiro登錄時,存放登錄物件資訊的位置 * @return */ public String getUserId(){ Subject localSubject = ThreadContext.getSubject(); String userId = (String) localSubject.getPrincipals().getPrimaryPrincipal(); return userId; } }
測驗方法:
/** * 靜態Mock需要使用PowerMockRunner * 并使用PrepareForTest,該測驗代表不會實際執行ThreadContext這個類 */ @RunWith(PowerMockRunner.class) @PrepareForTest({ThreadContext.class}) public class StaticComponentTest { @InjectMocks StaticComponent staticComponent; @Test public void getUserId(){ String userId = "12345"; PowerMockito.mockStatic(ThreadContext.class); Subject localSubject = PowerMockito.mock(Subject.class); when(ThreadContext.getSubject()).thenReturn(localSubject); PrincipalCollection principalCollection = PowerMockito.mock(PrincipalCollection.class); when(localSubject.getPrincipals()).thenReturn(principalCollection); when(principalCollection.getPrimaryPrincipal()).thenReturn("12345"); String resultUserId = staticComponent.getUserId(); Assert.assertTrue(userId.equals(resultUserId)); } }
3、方法內部有new物件的測驗
@Service public class CreateComponent { @Autowired private RestTemplate restTemplate; public MethodResult addUser(UserInfo user)throws Exception{ Map<String,String> map = new HashMap<>(); map.put("name",String.valueOf(user.getId())); map.put("email",user.getEmail()); map.put("nickname",user.getNickname()); MultiValueMap<String, String> header = new LinkedMultiValueMap(); header.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); HttpEntity request = new HttpEntity(JSONObject.toJSONString(map), header); String url = "http://127.0.0.1:8088/add/user"; try{ ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class); MethodResult result = JSONObject.parseObject(response.getBody(), MethodResult.class); return result; }catch (Exception e){ e.printStackTrace(); return null; } } }
測驗方法
@RunWith(PowerMockRunner.class) public class CreateComponentTest { @Mock private RestTemplate restTemplate; @InjectMocks private CreateComponent component; @Test public void createTest()throws Exception{ UserInfo param = new UserInfo(); param.setNickname("zhangsan"); param.setEmail("[email protected]"); param.setId(123); Map<String,String> map = new HashMap<>(); map.put("name",String.valueOf(param.getId())); map.put("email",param.getEmail()); map.put("nickname",param.getNickname()); MultiValueMap<String, String> header = new LinkedMultiValueMap(); header.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); HttpEntity request = new HttpEntity(JSONObject.toJSONString(map), header); PowerMockito.whenNew(HttpEntity.class).withAnyArguments().thenReturn(request); ResponseEntity responseEntity = PowerMockito.mock(ResponseEntity.class); Mockito.when(restTemplate.postForEntity("http://127.0.0.1:8088/add/user",request,String.class)).thenReturn(responseEntity); MethodResult result = new MethodResult(); result.setSucceed(true); PowerMockito.when(responseEntity.getBody()).thenReturn(JSONObject.toJSONString(result)); MethodResult methodResult = component.addUser(param); Assert.assertTrue(methodResult.isSucceed()); } }
4:方法過于復雜跳過內部私有方法,再單獨測驗私有方法
@Service public class PrivateComponent { public Integer entranceMethod(Integer i){ methodA(i); System.out.println("call methodA end"); i = methodB(i); System.out.println("call methodB end"); i = methodC(i); System.out.println("call methodC end"); return i; } private void methodA(Integer i){ System.out.println("do methodA i = " + i); methodA2(i); } private void methodA2(Integer i){ System.out.println("do methodA2 i = " + i); } private Integer methodB(Integer i){ ++i; System.out.println("do methodB"); return i; } private Integer methodC(Integer i){ ++i; System.out.println("do methodC"); return i; } }
測驗方法:
@RunWith(PowerMockRunner.class) @PrepareForTest(PrivateComponent.class) public class PrivateComponentTest { @InjectMocks private PrivateComponent privateComponent; /** * 測驗復雜的方法,跳過方法內部的私有方法:1、該私有方法沒有回傳值 * @throws Exception */ @Test public void jumpPrivateMethodTest()throws Exception{ PrivateComponent component = PowerMockito.spy(privateComponent); PowerMockito.doNothing().when(component,"methodA",1); Integer i = component.entranceMethod(1); System.out.println(i); Assert.assertTrue(i == 3); } /** * 測驗復雜的方法,跳過方法內部的私有方法:2、該私有方法有回傳值 * @throws Exception */ @Test public void jumpPrivateMethodTest2()throws Exception{ PrivateComponent component = PowerMockito.spy(privateComponent); PowerMockito.doReturn(5).when(component,"methodB", Mockito.any()); Integer i = component.entranceMethod(1); System.out.println(i); Assert.assertTrue(i == 6); } /** * 測驗復雜方法,單獨測驗方法內部的私有方法 * @throws Exception */ @Test public void privateMethodTest()throws Exception{ PrivateComponent component = PowerMockito.spy(privateComponent); Method method = PowerMockito.method(PrivateComponent.class,"methodB",Integer.class); Integer i = (Integer) method.invoke(component,1); System.out.println("result i = " + i); Assert.assertTrue(i == 2); } }
5、對controller進行測驗
@RestController @RequestMapping("/api/user") public class AController { @Autowired private AComponent aComponent; @GetMapping(value = "/info") public UserInfo testA1(Integer id){ UserInfo userInfo = aComponent.normalMethod(id); return userInfo; } @PostMapping(value = "/update") public String updateUserInfo(@RequestBody UserInfo userInfo){ aComponent.updateUserInfo(userInfo); return "success"; } }
測驗方法:
@RunWith(MockitoJUnitRunner.class) public class ControllerTest { @InjectMocks private AController aController; @Mock private AComponent aComponent; private MockMvc mockMvc; @Before public void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(aController).build(); } /** * 使用http GET方法呼叫的方式來測驗controller * @throws Exception */ @Test public void getControllerMvcTest() throws Exception { Integer id = 1; Mockito.when(aComponent.normalMethod(id)).thenReturn(getManUserInfo()); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/api/user/info?id="+id)) .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); String content = mvcResult.getResponse().getContentAsString(); Assert.assertNotNull(content); } /** * 使用http POST方法呼叫的方式來測驗controller * @throws Exception */ @Test public void postControllerMvcTest() throws Exception { UserInfo userInfo = getManUserInfo(); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/api/user/update") .contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(userInfo))) .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); String content = mvcResult.getResponse().getContentAsString(); Assert.assertTrue("success".equals(content)); } private UserInfo getManUserInfo(){ UserInfo userInfo = new UserInfo(); userInfo.setId(1); userInfo.setUserName("zhansan"); userInfo.setAge(12); userInfo.setSex("M"); return userInfo; } }
代碼地址:https://github.com/x104859/test-case
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549275.html
標籤:其他
