在个人上一篇文章中,咱们谈到了如何使用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,使依赖性管理变得简单。