在個人上一篇文章中,咱們談到了如何使用Parasoft Jtest的Unit Test Assistant高效地構建和改進這些測試。在這篇文章中,我將繼續討論測試任何複雜應用程序的最大挑戰之一:依賴性管理。html
說實話。複雜的應用程序並非從頭開始構建的——它們使用的是別人構建和維護的庫、API和核心項目或服務。做爲Spring的開發者,咱們儘量地利用現有的功能,這樣咱們就能夠把時間和精力花在咱們關心的事情上:應用程序的業務邏輯。咱們把細節留給庫,因此咱們的應用有不少依賴關係,以下圖橙色所示。java
圖1. 一個有多個依賴關係的Spring服務app
那麼,若是個人應用程序(控制器和服務)的大部分功能依賴於這些依賴的行爲,我如何將單元測試集中在個人應用程序上呢?最後,我是否是老是在執行集成測試而不是單元測試?若是我須要更好地控制這些依賴項的行爲,或者在單元測試期間依賴項不可用怎麼辦?框架
我須要的是一種將個人應用與這些依賴關係隔離開來的方法,這樣我就能夠將單元測試的重點放在個人應用代碼上。在某些狀況下,咱們能夠爲這些依賴關係建立專門的「測試」版本。然而,使用像Mockito這樣的標準化庫比這種方法有多種好處。dom
圖2. 一個模擬服務替換了多個依賴關係。jsp
通常來講,Spring應用程序將功能分割成Bean。一個Controller可能依賴於一個Service Bean,而Service Bean可能依賴於一個EntityManager、JDBC鏈接或另外一個Bean。大多數時候,須要將被測代碼與之隔離的依賴關係是Bean。在集成測試中,全部層都應該是真實的——但對於單元測試,咱們須要決定哪些依賴應該是真實的,哪些應該是mock。函數
Spring容許開發人員使用XML、Java或二者的結合來定義和配置bean,以便在你的配置中提供模擬和真實bean的混合。因爲mock對象須要在Java中定義,因此應該使用一個Configuration類來定義和配置mocked beans。單元測試
當UTA生成一個Spring測試時,你的控制器的全部依賴關係都被設置爲mock,這樣每一個測試都能得到對依賴關係的控制。當測試運行時,UTA會檢測在mock對象上對還沒有配置方法模擬的方法進行的方法調用,並建議這些方法應該被模擬。而後,咱們可使用快速修復來自動模擬每一個方法。測試
下面是一個依賴於PersonService的控制器示例:spa
@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註解的內部類,它使用Java配置爲被測Controller提供bean依賴。這樣咱們就能夠模擬bean方法中的PersonService。目前尚未模擬任何方法,因此當我運行測試時,我看到如下建議:
這意味着在我模擬的PersonService上調用了getAllPeople()方法,可是測試尚未爲這個方法配置模擬。當我選擇 "Mock it "快速修復選項時,測試就會更新:
@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,可是設置個人模擬依賴的挑戰已經解決了。
請注意,我能夠將生成的方法模擬從測試方法移到配置類的bean方法中。若是我這樣作,就意味着類中的每一個測試都會以一樣的方式模擬同一個方法。將方法模擬保留在測試方法中意味着該方法能夠在不一樣的測試之間以不一樣的方式進行模擬。
Spring Boot 使得 bean mocking 更加簡單。你沒必要爲測試中的 bean 使用 @Autowired 字段,也沒必要使用定義它的 Configuration 類,你只需爲 bean 使用一個字段並使用 @MockBean 來註釋它。Spring Boot 將使用它在 classpath 上找到的 mocking 框架爲 bean 建立一個 mock,並以注入容器中任何其餘 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 { ... } }
在上面的第一個例子中,Configuration類向Spring容器提供了全部的Bean。另外,你也可使用XML配置來代替Configuration類進行測試;或者你能夠將二者結合起來。例如,你可使用:
@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配置文件(這裏沒有顯示)來提供大部分的bean,這些bean能夠是真實的bean,也能夠是測試專用的bean。咱們還提供了一個@Configuration類,PersonService在這裏被模擬。@Primary註解表示,即便在XML配置中找到了PersonService bean,這個測試也會使用@Configuration類中的模擬bean來代替。這種類型的配置可使測試代碼更小,更容易管理。
你能夠配置UTA,使用你須要的任何特定的@ContextConfiguration屬性生成測試。
有時,依賴關係是靜態訪問的。例如,一個應用程序可能會經過靜態方法調用來訪問一個第三方服務。
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); }
在這個例子中,咱們的處理方法使用靜態方法調用從第三方服務中獲取Person對象。當咱們爲這個處理方法構建一個JUnit測試時,每次測試運行時都會對服務進行真正的HTTP調用,而不是模擬靜態的ExternalPersonService.getPerson()方法。
相反,讓咱們模擬靜態的ExternalPersonService.getPerson()方法。這樣就能夠避免HTTP調用,並容許咱們提供一個適合咱們測試需求的Person對象響應。單元測試助手能夠經過PowerMockito讓模擬靜態方法變得更容易。
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調用正在進行。讓咱們找到對ExternalPersonService.getPerson()的調用,並對其進行模擬:
測試已經更新爲使用PowerMock模擬靜態方法進行測試:
@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()); }
使用UTA,咱們如今能夠選擇getPersonResult變量並將其實例化,這樣模擬的方法調用就不會返回null:
String name = ""; // UTA: default value int age = 0; // UTA: default value Person getPersonResult = new Person(name, age);
當咱們再次運行測試時,getPersonResult從mockedExternalPersonService.getPerson()方法返回,測試經過。
注意:從流程樹中,還能夠選擇 "添加可模擬方法模式 "來進行靜態方法調用。這將配置Unit Test Assistant在生成新的測試時老是模擬這些靜態方法調用。
複雜的應用程序常常會有一些功能上的依賴性,這些依賴性會使開發人員對代碼進行單元測試的能力變得複雜並受到限制。使用像Mockito這樣的模擬框架能夠幫助開發人員將被測代碼與這些依賴關係隔離開來,使他們可以更快地編寫更好的單元測試。Parasoft Jtest 單元測試助手經過配置新的測試以使用 mock,以及在運行時查找缺失的方法 mock 並幫助開發人員爲其生成 mock,使依賴性管理變得簡單。