原創(chuàng)|使用教程|編輯:鄭恭琳|2021-01-22 09:59:11.580|閱讀 204 次
概述:Spring框架(以及Spring Boot)提供了一個有用的測試框架,可為您的Spring Controllers編寫JUnit測試。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
Spring框架(以及Spring Boot)提供了一個有用的測試框架,可為您的Spring Controllers編寫JUnit測試。
在我以前的文章中,我們討論了如何使用Parasoft Jtest的Unit Test Assistant有效地構(gòu)建和改進這些測試。在本文中,我將繼續(xù)解決測試任何復雜應用程序的最大挑戰(zhàn)之一:依賴關系管理。
說實話。復雜的應用程序不是從頭開始構(gòu)建的,而是使用庫,API以及由其他人構(gòu)建和維護的核心項目或服務。作為Spring開發(fā)人員,我們盡可能地利用現(xiàn)有功能,因此我們可以將時間和精力花在我們關心的方面:應用程序的業(yè)務邏輯。我們將詳細信息留給庫,因此我們的應用程序具有很多依賴性,如下面的橙色所示:
圖1.具有多個依賴項的Spring服務
如果單元測試的大多數(shù)功能取決于這些依賴項的行為,那么如何將單元測試集中在應用程序(控制器和服務)上?最后,我不是總是執(zhí)行集成測試而不是單元測試嗎?如果我需要更好地控制這些依賴項的行為,或者在單元測試期間這些依賴項不可用,該怎么辦?
我需要的是一種將應用程序與這些依賴項隔離的方法,因此我可以將單元測試的重點放在應用程序代碼上。在某些情況下,我們可以為這些依賴項創(chuàng)建專門的“測試”版本。但是,由于多種原因,使用Mockito之類的標準化庫相對于此方法有很多好處:
圖2.模擬服務替換了多個依賴關系
Spring的依賴
通常,Spring應用程序?qū)⒐δ懿鸱譃?/span>Bean。控制器可能取決于Service Bean,而Service Bean可能取決于EntityManager,JDBC連接或另一個Bean。在大多數(shù)情況下,需要隔離測試代碼的依賴是bean。在集成測試中,所有層都應該是真實的,但是對于單元測試,我們需要確定哪些依賴項應該是真實的,哪些應該是模擬的。
Spring允許開發(fā)人員使用XML,Java或兩者的結(jié)合來定義和配置bean,以在您的配置中混合使用模擬bean和實際bean。由于需要在Java中定義模擬對象,因此應使用Configuration類來定義和配置模擬的bean。
當UTA生成Spring測試時,控制器的所有依賴項都將設置為模擬,以便每個測試都可以控制該依賴項。運行測試時,UTA會檢測尚未配置方法模擬的方法在模擬對象上進行的方法調(diào)用,并建議應模擬那些方法。然后,我們可以使用快速修復程序自動模擬每種方法。
這是依賴于PersonService的示例控制器:
@Controller @RequestMapping("/people") public class PeopleController { @Autowired protected PersonService personService; @GetMapping public ModelAndView people(Model model){ for (Person person : personService.getAllPeople()) { model.addAttribute(person.getName(), person.getAge()); } return new ModelAndView("people.jsp", model.asMap()); } }
還有由Parasoft Jtest的單元測試助手生成的示例測試:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class PeopleControllerTest { @Autowired PersonService personService; // Other fields and setup @Configuration static class Config { // Other beans @Bean public PersonService getPersonService() { return mock(PersonService.class); } } @Test public void testPeople() throws Exception { // When ResultActions actions = mockMvc.perform(get("/people")); } }
在這里,測試使用帶有@Configuration注釋的內(nèi)部類,該類使用Java配置為受測控制器提供Bean依賴關系。這使我們可以在bean方法中模擬PersonService。還沒有模擬方法,因此在運行測試時,我看到以下建議:
這意味著在我模擬的PersonService上調(diào)用了getAllPeople()方法,但是測試尚未為此方法配置模擬。當我選擇“模擬”快速修復選項時,測試將更新:
@Test public void testPeople() throws Exception { Collection<Person> getAllPeopleResult = new ArrayList<Person>(); doReturn(getAllPeopleResult).when(personService).getAllPeople(); // When ResultActions actions = mockMvc.perform(get("/people"));
當我再次運行測試時,它通過了。我仍然應該填充由getAllPeople()返回的Collection,但是解決了設置我的模擬依賴項的挑戰(zhàn)。
請注意,我可以將生成的方法模擬從測試方法移到Configuration類的bean方法中。如果這樣做,則意味著該類中的每個測試都將以相同的方式模擬相同的方法。將方法模擬保留在測試方法中意味著可以在不同的測試之間對方法進行不同的模擬。
Spring Boot
Spring Boot使Bean模擬變得更加容易。無需為測試中的bean和定義它的Configuration類使用@Autowired字段,您只需使用bean的字段并使用@MockBean對其進行注釋即可。 Spring Boot將使用它在類路徑上找到的模擬框架為該bean創(chuàng)建一個模擬,并以可以注入容器中任何其他bean的方式注入它。當使用單元測試助手生成Spring Boot測試時,使用@MockBean功能而不是Configuration類。
@SpringBootTest @AutoConfigureMockMvc public class PeopleControllerTest { // Other fields and setup – no Configuration class needed! @MockBean PersonService personService; @Test public void testPeople() throws Exception { ... } }
XML與Java配置
在上面的第一個示例中,Configuration類將所有bean提供給Spring容器。另外,您可以將XML配置用于測試,而不是Configuration類。或者您可以將兩者結(jié)合起來。例如:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({ "classpath:/**/testContext.xml" }) public class PeopleControllerTest { @Autowired PersonService personService; // Other fields and setup @Configuration static class Config { @Bean @Primary public PersonService getPersonService() { return mock(PersonService.class); } } // Tests }
在這里,該類在@ContextConfiguration批注(此處未顯示)中引用XML配置文件以提供大多數(shù)bean,這些bean可以是真實bean或特定于測試的bean。 我們還提供了@Configuration類,其中模擬了PersonService。@Primary批注指示即使在XML配置中找到了PersonService bean,該測試也將使用@Configuration類中的模擬bean。這種類型的配置可以使測試代碼更小、更易于管理。
您可以使用所需的任何特定@ContextConfiguration屬性將UTA配置為生成測試。
模擬靜態(tài)方法
有時,依賴項是靜態(tài)訪問的。例如,應用程序可能通過靜態(tài)方法調(diào)用訪問第三方服務:
public class ExternalPersonService { public static Person getPerson(int id) { RestTemplate restTemplate = new RestTemplate(); try { return restTemplate.getForObject("http://domain.com/people/" + id, Person.class); } catch (RestClientException e) { return null; } } }
在我們的控制器中:
@GetMapping public ResponseEntity<Person> getPerson(@PathVariable("id") int id, Model model) { Person person = ExternalPersonService.getPerson(id); if (person != null) { return new ResponseEntity<Person>(person, HttpStatus.OK); } return new ResponseEntity<>(HttpStatus.NOT_FOUND); }
在此示例中,我們的處理程序方法使用靜態(tài)方法調(diào)用從第三方服務獲取Person對象。當我們?yōu)榇颂幚沓绦蚍椒?gòu)建JUnit測試時,每次運行測試時都會對服務進行真正的HTTP調(diào)用。
相反,讓我們模擬靜態(tài)的ExternalPersonService.getPerson()方法。這可以防止HTTP調(diào)用,并允許我們提供適合我們測試需求的Person對象響應。單元測試助手可以使使用PowerMockito模擬靜態(tài)方法更加容易。
UTA為上面的處理程序方法生成一個測試,如下所示:
@Test public void testGetPerson() throws Throwable { // When long id = 1L; ResultActions actions = mockMvc.perform(get("/people/" + id)); // Then actions.andExpect(status().isOk()); }
運行測試時,我們將看到在UTA流樹中進行的HTTP調(diào)用。讓我們找到對ExternalPersonService.getPerson()的調(diào)用,并對其進行模擬:
測試已更新為使用PowerMock模擬此測試的靜態(tài)方法:
@Test public void testGetPerson() throws Throwable { spy(ExternalPersonService.class); Person getPersonResult = null; // UTA: default value doReturn(getPersonResult).when(ExternalPersonService.class, "getPerson", anyInt()); // When int id = 0; ResultActions actions = mockMvc.perform(get("/people/" + id)); // Then actions.andExpect(status().isOk()); }
現(xiàn)在,使用UTA,我們可以選擇getPersonResult變量并將其實例化,以便模擬方法調(diào)用不會返回null:
String name = ""; // UTA: default value int age = 0; // UTA: default value Person getPersonResult = new Person(name, age);
當我們再次運行測試時,getPersonResult從模擬的ExternalPersonService.getPerson()方法返回,并且測試通過。
注意:從流樹中,您還可以為靜態(tài)方法調(diào)用選擇“添加可模擬方法模式”。這會將單元測試助手配置為在生成新測試時始終模擬那些靜態(tài)方法調(diào)用。
復雜的應用程序通常具有功能依賴性,這會使開發(fā)人員對代碼進行單元測試的能力復雜化并受到限制。使用Mockito這樣的模擬框架可以幫助開發(fā)人員將測試中的代碼與這些依賴項隔離開來,從而使他們能夠更快地編寫更好的單元測試。Parasoft Jtest單元測試助手通過配置新的測試以使用模擬,并在運行時查找丟失的方法模擬并幫助開發(fā)人員為它們生成模擬,使依賴關系管理變得容易。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務必注明出處、不得修改原文相關鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn