赵走x博客
网站访问量:151447
首页
书籍
软件
工具
古诗词
搜索
登录
11、测试运行中的应用程序
10、测试 Web 应用程序
9、集成测试自动配置
8、定制应用程序错误页面
7、通过属性文件外置配置
6、覆盖 Spring Boot 自动配置
5、使用自动配置
4、使用起步依赖
3、运用 Spring Boot
2、Spring Boot 入门
1、Spring 风云再起
10、测试 Web 应用程序
资源编号:76580
Java
Spring Boot实战
热度:112
Spring MVC有一个优点:它的编程模型是围绕POJO展开的,在POJO上添加注解,声明如何 处理Web请求。这种编程模型不仅简单,还让你能像对待应用程序中的其他组件一样对待这些控 制器。你还可以针对这些控制器编写测试,就像测试POJO一样。 举例来说,考虑ReadingListController里的addToReadingList()方法: ``` @RequestMapping(method=RequestMethod.POST) public String addToReadingList(Book book) { book.setReader(reader); readingListRepository.save(book); return "redirect:/readingList"; } ``` 如果忽略@RequestMapping注解,你得到的就是一个相当基础的Java方法。你立马就能想 到这样一个测试,提供一个ReadingListRepository的模拟实现,直接调用addToReadingList(),判断返回值并验证对ReadingListRepository的save()方法有过调用。 该测试的问题在于,它仅仅测试了方法本身,当然,这要比没有测试好一点。然而,它没有 测试该方法处理/readingList的POST请求的情况,也没有测试表单域绑定到Book参数的情况。虽 然你可以判断返回的String包含特定值,但没法明确测试请求在方法处理完之后是否真的会重 定向到/readingList。 要恰当地测试一个Web应用程序,你需要投入一些实际的HTTP请求,确认它能正确地处理 那些请求。幸运的是,Spring Boot开发者有两个可选的方案能实现这类测试。 Spring Mock MVC:能在一个近似真实的模拟Servlet容器里测试控制器,而不用实际启动 应用服务器。 Web集成测试:在嵌入式Servlet容器(比如Tomcat或Jetty)里启动应用程序,在真正的应 用服务器里执行测试。 这两种方法各有利弊。很明显,启动一个应用服务器会比模拟Servlet容器要慢一些,但毫无 疑问基于服务器的测试会更接近真实环境,更接近部署到生产环境运行的情况。 接下来,你会看到如何使用Spring Mock MVC测试框架来测试Web应用程序。然后,在4.3节 里你会看到如何为运行在应用服务器里的应用程序编写测试。 # 4.2.1 模拟 Spring MVC 早在Spring 3.2, Spring Framework就有了一套非常实用的Web应用程序测试工具,能模拟 Spring MVC,不需要真实的Servlet容器也能对控制器发送HTTP请求。Spring的Mock MVC框架模 拟了Spring MVC的很多功能。它几乎和运行在Servlet容器里的应用程序一样,尽管实际情况并非 如此。 要在测试里设置Mock MVC,可以使用MockMvcBuilders,该类提供了两个静态方法。 standaloneSetup():构建一个Mock MVC,提供一个或多个手工创建并配置的控制器。 webAppContextSetup():使用Spring应用程序上下文来构建Mock MVC,该上下文里可以包含一个或多个配置好的控制器。 两者的主要区别在于,standaloneSetup()希望你手工初始化并注入你要测试的控制器, 而webAppContextSetup()则基于一个WebApplicationContext的实例,通常由Spring加载。 前者同单元测试更加接近,你可能只想让它专注于单一控制器的测试,而后者让Spring加载控制 器及其依赖,以便进行完整的集成测试。 我们要用的是 webAppContextSetup() 。 Spring完成了 ReadingListController 的初始 化,并从Spring Boot自动配置的应用程序上下文里将其注入,我们直接对其进行测试。 webAppContextSetup()接受一个WebApplicationContext参数。因此,我们需要为测 试类加上@WebAppConfiguration注解,使用@Autowired将WebApplicationContext作为实 例变量注入测试类。代码清单4-2演示了Mock MVC测试的执行入口。 代码清单4-2:为集成测试控制器创建Mock MVC ``` @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration( classes = ReadingListApplication.class) @WebAppConfiguration public class MockMvcWebTests { @Autowired private WebApplicationContext webContext; private MockMvc mockMvc; @Before public void setupMockMvc() { 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()))); } ``` 现在这个测试方法读起来就很自然了。首先向/readingList发起一个GET请求,接下来希望该 请求处理成功(isOk()会判断HTTP 200响应码),并且视图的逻辑名称为readingList。测试 还要断定模型包含一个名为books的属性,该属性是一个空集合。所有的断言都很直观。 值得一提的是,此处完全不需要将应用程序部署到Web服务器上,它是运行在模拟的Spring MVC中的,刚好能通过MockMvc实例处理我们给它的HTTP请求。 太酷了,不是吗? 让我们再来看一个测试方法,这次会更有趣,我们实际发送一个HTTP POST请求提交一本新 书。我们应该期待POST请求处理后重定向回/readingList,模型将包含新添加的图书。代码清单4-3 演示了如何通过Spring的Mock MVC来实现这个测试。 代码清单4-3:测试提交一本新书 ``` @Test public void postBook() throws Exception { mockMvc.perform(post("/readingList") .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")) .andExpect(status().isOk()) .andExpect(view().name("readingList")) .andExpect(model().attributeExists("books")) .andExpect(model().attribute("books", hasSize(1))) .andExpect(model().attribute("books", contains(samePropertyValuesAs(expectedBook)))); } ``` 很明显,代码清单4-3里的测试更加复杂,实际上是两个测试放在一个方法里。第一部分提交图书并检查了请求的结果,第二部分执行了一次对主页的GET请求,检查新建的图书是否在模 型中。 在提交图书时,我们必须确保内容类型(通过MediaType.APPLICATION_FORM_URLENCODED) 设置为application/x-www-form-urlencoded,这才是运行应用程序时浏览器会发送的内容类型。随 后,要用MockMvcRequestBuilders的param方法设置表单域,模拟要提交的表单。一旦请求 执行,我们要检查响应是否是一个到/readingList的重定向。 假定以上测试都通过,我们进入第二部分。首先设置一个Book对象,包含想要的值。我们 用这个对象和首页获取的模型的值进行对比。 随后要对/readingList发起一个GET请求,大部分内容和我们之前测试主页时一样,只是之前 模型中有一个空集合,而现在有一个集合项。这里要检查它的内容是否和我们创建的expectedBook一致。如此一来,我们的控制器看来保存了发送给它的图书,完成了工作。 至此,这些测试验证了一个未经保护的应用程序,和我们在第2章里写的应用程序很类似。 但如果我们想要测试一个安全加固过的应用程序(比如我们在第3章里写的程序),又该怎么办? # 4.2.2 测试 Web 安全 Spring Security能让你非常方便地测试安全加固后的Web应用程序。为了利用这点优势,你必 须在项目里添加Spring Security的测试模块。 要在Gradle里做到这一点, 你需要的就是以下 testCompile依赖: ``` testCompile("org.springframework.security:spring-security-test") ``` 如果你用的是Maven,则添加以下
: ```
org.springframework.security
spring-security-test
test
``` 应用程序的Classpath里有了Spring Security的测试模块之后,只需在创建MockMvc实例时运用 Spring Security的配置器。 ``` @Before public void setupMockMvc() { mockMvc = MockMvcBuilders .webAppContextSetup(webContext) .apply(springSecurity()) .build(); } ``` springSecurity()方法返回了一个Mock MVC配置器,为Mock MVC开启了Spring Security 支持。只需像上面这样运用就行了,Spring Security会介入MockMvc上执行的每个请求。具体的 安全配置取决于你如何配置Spring Security(或者Spring Boot如何自动配置Spring Security)。在阅读列表这个应用程序里,我们在第3章里创建SecurityConfig.java时,配置也是如此。 ##### springSecurity()方法 springSecurity()是SecurityMockMvcConfigurers的一个静态方法,考虑到可读性,我已经将其静态导入。 开启了Spring Security之后,在请求主页的时候,我们便不能只期待HTTP 200响应。如果请 求未经身份验证,我们应该期待重定向到登录页面: ``` @Test public void homePage_unauthenticatedUser() throws Exception { mockMvc.perform(get("/")) .andExpect(status().is3xxRedirection()) .andExpect(header().string("Location", "http://localhost/login")); } ``` 不过,经过身份验证的请求又该如何发起呢?Spring Security提供了两个注解。 @WithMockUser:加载安全上下文,其中包含一个UserDetails,使用了给定的用户名、 密码和授权。 @WithUserDetails:根据给定的用户名查找UserDetails对象,加载安全上下文。 在这两种情况下,Spring Security的安全上下文都会加载一个UserDetails对象,添加了该 注解的测试方法在运行过程中都会使用该对象。@WithMockUser注解是两者里比较基础的那个, 允许显式声明一个UserDetails,并加载到安全上下文。 ``` @Test @WithMockUser(username="craig", password="password", roles="READER") public void homePage_authenticatedUser() throws Exception { ... } ``` 如你所见,@WithMockUser绕过了对UserDetails对象的正常查询,用给定的值创建了一 个UserDetails对象取而代之。在简单的测试里,这就够用了。但我们的测试需要Reader(实 现 了 UserDetails ) 而 非 @WithMockUser 创 建 的 通 用 UserDetails 。 为 此 , 我 们 需 要 @WithUserDetails。 @WithUserDetails注解使用事先配置好的UserDetailsService来加载UserDetails对 象。回想一下第3章,我们配置了一个UserDetailsService Bean,它会根据给定的用户名查找 并返回一个Reader对象。太完美了!所以我们要为测试方法添加@WithUserDetails注解,如 代码清单4-4所示。 代码清单4-4:测试带有用户身份验证的安全加固方法 ``` @Test @WithUserDetails("craig") public void homePage_authenticatedUser() throws Exception { Reader expectedReader = new Reader(); expectedReader.setUsername("craig"); expectedReader.setPassword("password"); expectedReader.setFullname("Craig Walls"); mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(view().name("readingList")) .andExpect(model().attribute("reader", samePropertyVal uesAs(expectedReader))) .andExpect(model().attribute("books", hasSize(0))) } ``` 在代码清单4-4里,我们通过@WithUserDetails注解声明要在测试方法执行过程中向安全 上下文里加载craig用户。Reader会放入模型,该测试方法先创建了一个期望的Reader对象,后 续可以用来进行比较。随后GET请求发起,也有了针对视图名和模型内容的断言,其中包括名为 reader的模型属性。 同样,此处没有启动Servlet容器来运行这些测试,Spring的Mock MVC取代了实际的Servlet 容器。这样做的好处是测试方法运行相对较快。因为不需要等待服务器启动,而且不需要打开 Web浏览器发送表单,所以测试比较简单快捷。 不过,这并不是一个完整的测试。它比直接调用控制器方法要好,但它并没有真的在Web浏 览器里执行应用程序,验证呈现出的视图。为此,我们需要启动一个真正的Web服务器,用真实 浏览器来访问它。让我们来看看Spring Boot如何启动一个真实的Web服务器来帮助测试。