1.Spring Framework的核心工作是将所有组件编织在一起,构成一个应用程序。整个过程就是读取配置说明(可以是XML、基于Java的配置、基于Groovy的配置或其他类型的配置),在应用程序上下文里初始化Bean,将Bean注入依赖它们的其他Bean中。
2.对Spring应用程序进行集成测试时,让Spring遵照生产环境来组装测试目标Bean是非常重要的一点。
3.自Spring 2.5开始,集成测试支持的形式就变成了SpringJUnit4ClassRunner。这是一个JUnit类运行器,会为JUnit测试加载Spring应用程序上下文,并为测试类自动织入所需的Bean。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( //加载应用程序上下文
classes=AddressBookConfiguration.class)
public class AddressServiceTests {
@Autowired
private AddressService addressService; //注入地址服务
@Test
public void testService() { //测试地址服务
Address address = addressService.findByLastName("Sheman");
assertEquals("P", address.getFirstName());
assertEquals("Sherman", address.getLastName());
assertEquals("42 Wallaby Way", address.getAddressLine1());
assertEquals("Sydney", address.getCity());
assertEquals("New South Wales", address.getState());
assertEquals("2000", address.getPostCode());
}
}
@RunWith的参数是SpringJunit4ClassRunner.class,开启了Spring集成测试支持。
@ContextConfiguration指定了如何加载应用程序上下文。
SpringJunit4ClassRunner还能通过自动织入从应用程序上下文里向测试本身注入Bean。
testService()方法调用地址服务并验证了结果。
在Spring 4.2里,可以选择基于规则的SpringClassRule和SpringMethodRule来代替SpringJunit4ClassRunner.
虽然@ContextConfiguration在加载Spring应用上下文的过程种做了很多事情,但是它没能加载完整的Spring Boot 。
Spring Boot最总由SpringApplication加载的。
SpringApplication不仅加载应用程序上下文,还会开启日志、加载外部属性(application.properties或application.yml),以及其他特性,用@ContextConfiguration则得不到这些特性。
要在集成测试里或获取这些特性,可以把@ContextConfiguration替换为Spring Boot的@SpringApplicationConfiguration。
要测试,需要投入一些实际的的HTTP请求,确认它能正确地处理
那些请求。幸运的是,Spring Boot开发者有两个可选的方案能实现这类测试。
1.Spring Mock MVC:能在一个近似真实的模拟Servlet容器里测试控制器,而不用实际启动
应用服务器。
2.Web集成测试:在嵌入式Servlet容器(比如Tomcat或Jetty)里启动应用程序,在真正的应
用服务器里执行测试。
Spring 的 Mock MVC框架模拟了SpringMVC的很多功能。
要在测试里设置Mock MVC,可以使用MockMvcBuilders,该类提供了两个静态方法。
1.standaloneSetup():构建一个Mock MVC,提供一个或多个手工创建并配置的控制器。
2.webAppContextSetup():使用Spring应用程序上下文来构建Mock MVC,该上下文里可以包含一个或多个配置好的控制器
两者的主要区别在于,standaloneSetup()希望你手工初始化并注入你要测试的控制器,而webAppContextSetup()则基于一个WebApplicationContext的实例,通常由Spring加载。
前者同单元测试更加接近,你可能只想让它专注于单一控制器的测试,而后者让Spring加载控制器及其依赖,以便进行完整的集成测试。
我们要用的是webAppContextSetup()。Spring完成了ReadingListController的初始化,并从Spring Boot自动配置的应用程序上下文里将其注入,我们直接对其进行测试。
webAppContextSetup()接受一个WebApplicationContext参数。因此,我们需要为测试类加上@WebAppConfiguration注解,使用@Autowired将WebApplicationContext作为实例变量注入测试类。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(
classes = ReadingListApplication.class)
@WebAppConfiguration //开启web上下文测试
public class MockMvcWebTests {
@Autowired
private WebApplicationContext webContext; //注入WebApplicationContext
private MockMvc mockMvc;
@Before
public void setupMockMvc() { //设置MockMVC
mockMvc = MockMvcBuilders
.webAppContextSetup(webContext)
.build();
}
}
@WebAppConfiguration注解声明,由SpringJUnit4ClassRunner创建的应用程序上下文应该是一个WebApplicationContext(相对于基本的非WebApplicationContext)。setupMockMvc()方法上添加了JUnit的@Before注解,表明它应该在测试方法之前执行。它将WebApplicationContext注入webAppContextSetup()方法,然后调用build()产生了一个MockMvc实例,该实例赋给了一个实例变量,供测试方法使用。
现在我们有了一个MockMvc,已经可以开始写测试方法了。我们先写个简单的测试方法,向/readingList发送一个HTTP GET请求,判断模型和视图是否满足我们的期望。下面的homePage()测试方法就是我们所需要的。
@Test
public void homePage() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/readingList"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("readingList"))
.andExpect(MockMvcResultMatchers.model().attributeExists("books"))
.andExpect(MockMvcResultMatchers.model().attribute("books",
Matchers.is(Matchers.empty())));
}
如你所见,我们在这个测试方法里使用了很多静态方法,包括Spring的MockMvcRequestBuilders和MockMvcResultMatchers里的静态方法,还有Hamcrest库的Matchers里的静态方法。在深入探讨这个测试方法前,先添加一些静态import,这样代码看起来更清爽一些:
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.
➥ MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.
➥ MockMvcResultMatchers.*;
有了这些静态import后,测试方法可以稍作调整:
@Test
public void homePage() throws Exception {
mockMvc.perform(get("/readingList"))
.andExpect(status().isOk())
.andExpect(view().name("readingList"))
.andExpect(model().attributeExists("books"))
.andExpect(model().attribute("books", is(empty())));
}
此处完全不需要将应用程序部署到Web服务器上,它是运行在模拟的Spring MVC中的,刚好能通过MockMvc实例处理我们给它的HTTP请求。
再来看一个测试方法,发送一个HTTP POST请求提交一本新书。我们应该期待POST请求处理后重定向回/readingList,模型将包含新添加的图书。
public void postBook() throws Exception {
mockMvc.perform(post("/readingList") //执行post请求
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "BOOK TITLE")
.param("author", "BOOK AUTHOR")
.param("isbn", "1234567890")
.param("description", "DESCRIPTION"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location", "/readingList"));
Book expectedBook = new Book(); //配置期望的图书
expectedBook.setId(1L);
expectedBook.setReader("craig");
expectedBook.setTitle("BOOK TITLE");
expectedBook.setAuthor("BOOK AUTHOR");
expectedBook.setIsbn("1234567890");
expectedBook.setDescription("DESCRIPTION");
mockMvc.perform(get("/readingList")) //执行get请求
.andExpect(status().isOk())
.andExpect(view().name("readingList"))
.andExpect(model().attributeExists("books"))
.andExpect(model().attribute("books", hasSize(1)))
.andExpect(model().attribute("books",
contains(samePropertyValuesAs(expectedBook))));
}
在提交图书时,我们必须确保内容类型(通过MediaType.APPLICATION_FORM_URLENCODED)设置为application/x-www-form-urlencoded,这才是运行应用程序时浏览器会发送的内容类型。随后,要用MockMvcRequestBuilders的param方法设置表单域,模拟要提交的表单。一旦请求执行,我们要检查响应是否是一个到/readingList的重定向。
假定以上测试都通过,我们进入第二部分。首先设置一个Book对象,包含想要的值。我们用这个对象和首页获取的模型的值进行对比。
随后要对/readingList发起一个GET请求,大部分内容和我们之前测试主页时一样,只是之前模型中有一个空集合,而现在有一个集合项。这里要检查它的内容是否和我们创建的expectedBook一致。如此一来,我们的控制器看来保存了发送给它的图书,完成了工作。