返回介绍

4.2.1 模拟 Spring MVC

发布于 2025-04-21 21:10:08 字数 5861 浏览 0 评论 0 收藏

早在 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 注解,使用 @AutowiredWebApplicationContext 作为实例变量注入测试类。代码清单 4-2 演示了 Mock MVC 测试的执行入口。

代码清单 4-2 为集成测试控制器创建 Mock MVC

@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 = MockMvcBuilders       ←---设置 MockMvc
      .webAppContextSetup(webContext)
      .build();
  }

}

@WebAppConfiguration 注解声明,由 SpringJUnit4ClassRunner 创建的应用程序上下文应该是一个 WebApplicationContext (相对于基本的非 Web ApplicationContext )。

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 的 MockMvcRequestBuildersMockMvcResultMatchers 里的静态方法,还有 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")       ←---执行 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))));
}

很明显,代码清单 4-3 里的测试更加复杂,实际上是两个测试放在一个方法里。第一部分提交图书并检查了请求的结果,第二部分执行了一次对主页的 GET 请求,检查新建的图书是否在模型中。

在提交图书时,我们必须确保内容类型(通过 MediaType.APPLICATION_FORM_URLENCODED )设置为 application/x-www-form-urlencoded,这才是运行应用程序时浏览器会发送的内容类型。随后,要用 MockMvcRequestBuildersparam 方法设置表单域,模拟要提交的表单。一旦请求执行,我们要检查响应是否是一个到/readingList 的重定向。

假定以上测试都通过,我们进入第二部分。首先设置一个 Book 对象,包含想要的值。我们用这个对象和首页获取的模型的值进行对比。

随后要对/readingList 发起一个 GET 请求,大部分内容和我们之前测试主页时一样,只是之前模型中有一个空集合,而现在有一个集合项。这里要检查它的内容是否和我们创建的 expectedBook 一致。如此一来,我们的控制器看来保存了发送给它的图书,完成了工作。

至此,这些测试验证了一个未经保护的应用程序,和我们在第 2 章里写的应用程序很类似。但如果我们想要测试一个安全加固过的应用程序(比如我们在第 3 章里写的程序),又该怎么办?

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。