standaloneSetup 使用案例详解
standaloneSetup
是 MockMvc 提供的另一种构建方式,与 webAppContextSetup
相比,它更轻量级,适合针对单个控制器的单元测试。下面通过多个详细案例展示其使用方法。
一、基础使用案例
1. 简单控制器测试
假设有一个简单的控制器:
java
@RestController
@RequestMapping("/api")
public class SimpleController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World";
}
}
@RestController
@RequestMapping("/api")
public class SimpleController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World";
}
}
测试类:
java
public class SimpleControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new SimpleController()) // 只初始化这一个控制器
.build();
}
@Test
void testSayHello() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello World"));
}
}
public class SimpleControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new SimpleController()) // 只初始化这一个控制器
.build();
}
@Test
void testSayHello() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello World"));
}
}
二、带依赖的控制器测试
1. 模拟服务层依赖
控制器:
java
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
}
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
}
测试类:
java
public class ProductControllerTest {
private MockMvc mockMvc;
private ProductService productService = mock(ProductService.class);
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new ProductController(productService))
.build();
}
@Test
void testGetProduct() throws Exception {
// 准备模拟数据
Product mockProduct = new Product(1L, "Laptop", 999.99);
when(productService.getProductById(1L)).thenReturn(mockProduct);
// 执行测试
mockMvc.perform(get("/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Laptop"))
.andExpect(jsonPath("$.price").value(999.99));
}
}
public class ProductControllerTest {
private MockMvc mockMvc;
private ProductService productService = mock(ProductService.class);
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new ProductController(productService))
.build();
}
@Test
void testGetProduct() throws Exception {
// 准备模拟数据
Product mockProduct = new Product(1L, "Laptop", 999.99);
when(productService.getProductById(1L)).thenReturn(mockProduct);
// 执行测试
mockMvc.perform(get("/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Laptop"))
.andExpect(jsonPath("$.price").value(999.99));
}
}
三、高级配置案例
1. 添加拦截器
java
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("X-Auth-Token");
if (!"valid-token".equals(token)) {
response.sendError(401, "Invalid token");
return false;
}
return true;
}
}
// 测试类
public class SecureControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new SecureController())
.addInterceptors(new AuthInterceptor()) // 添加拦截器
.build();
}
@Test
void testWithoutToken() throws Exception {
mockMvc.perform(get("/secure/data"))
.andExpect(status().isUnauthorized());
}
@Test
void testWithValidToken() throws Exception {
mockMvc.perform(get("/secure/data")
.header("X-Auth-Token", "valid-token"))
.andExpect(status().isOk());
}
}
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("X-Auth-Token");
if (!"valid-token".equals(token)) {
response.sendError(401, "Invalid token");
return false;
}
return true;
}
}
// 测试类
public class SecureControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new SecureController())
.addInterceptors(new AuthInterceptor()) // 添加拦截器
.build();
}
@Test
void testWithoutToken() throws Exception {
mockMvc.perform(get("/secure/data"))
.andExpect(status().isUnauthorized());
}
@Test
void testWithValidToken() throws Exception {
mockMvc.perform(get("/secure/data")
.header("X-Auth-Token", "valid-token"))
.andExpect(status().isOk());
}
}
2. 配置消息转换器
java
public class ProductControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
// 创建自定义的消息转换器列表
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new StringHttpMessageConverter());
mockMvc = MockMvcBuilders
.standaloneSetup(new ProductController(mock(ProductService.class)))
.setMessageConverters(converters) // 设置消息转换器
.build();
}
}
public class ProductControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
// 创建自定义的消息转换器列表
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new StringHttpMessageConverter());
mockMvc = MockMvcBuilders
.standaloneSetup(new ProductController(mock(ProductService.class)))
.setMessageConverters(converters) // 设置消息转换器
.build();
}
}
四、异常处理测试
1. 测试控制器异常处理
控制器:
java
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
if (id <= 0) {
throw new IllegalArgumentException("Invalid user ID");
}
return new User(id, "John Doe");
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
if (id <= 0) {
throw new IllegalArgumentException("Invalid user ID");
}
return new User(id, "John Doe");
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}
测试类:
java
public class UserControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new UserController())
.setControllerAdvice(new ExceptionHandler()) // 可以添加全局异常处理器
.build();
}
@Test
void testValidUser() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void testInvalidUser() throws Exception {
mockMvc.perform(get("/users/0"))
.andExpect(status().isBadRequest())
.andExpect(content().string("Invalid user ID"));
}
}
public class UserControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new UserController())
.setControllerAdvice(new ExceptionHandler()) // 可以添加全局异常处理器
.build();
}
@Test
void testValidUser() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void testInvalidUser() throws Exception {
mockMvc.perform(get("/users/0"))
.andExpect(status().isBadRequest())
.andExpect(content().string("Invalid user ID"));
}
}
五、RESTful API 完整测试案例
1. 完整CRUD控制器测试
控制器:
java
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
@PostMapping
public ResponseEntity<Book> addBook(@RequestBody Book book) {
Book saved = bookRepository.save(book);
return ResponseEntity.created(URI.create("/api/books/" + saved.getId()))
.body(saved);
}
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Book not found"));
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
if (!bookRepository.existsById(id)) {
throw new ResourceNotFoundException("Book not found");
}
book.setId(id);
return bookRepository.save(book);
}
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
bookRepository.deleteById(id);
}
}
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
@PostMapping
public ResponseEntity<Book> addBook(@RequestBody Book book) {
Book saved = bookRepository.save(book);
return ResponseEntity.created(URI.create("/api/books/" + saved.getId()))
.body(saved);
}
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Book not found"));
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
if (!bookRepository.existsById(id)) {
throw new ResourceNotFoundException("Book not found");
}
book.setId(id);
return bookRepository.save(book);
}
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
bookRepository.deleteById(id);
}
}
测试类:
java
public class BookControllerTest {
private MockMvc mockMvc;
private BookRepository bookRepository = mock(BookRepository.class);
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new BookController(bookRepository))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
}
@Test
void testGetAllBooks() throws Exception {
when(bookRepository.findAll()).thenReturn(Arrays.asList(
new Book(1L, "Spring in Action", "Craig Walls"),
new Book(2L, "Clean Code", "Robert Martin")
));
mockMvc.perform(get("/api/books"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].title").value("Spring in Action"))
.andExpect(jsonPath("$[1].title").value("Clean Code"));
}
@Test
void testAddBook() throws Exception {
Book newBook = new Book(null, "New Book", "Author");
Book savedBook = new Book(1L, "New Book", "Author");
when(bookRepository.save(any(Book.class))).thenReturn(savedBook);
mockMvc.perform(post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"New Book\",\"author\":\"Author\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/books/1")))
.andExpect(jsonPath("$.id").value(1));
}
@Test
void testGetBookNotFound() throws Exception {
when(bookRepository.findById(1L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/books/1"))
.andExpect(status().isNotFound());
}
// 其他CRUD操作的测试类似...
}
public class BookControllerTest {
private MockMvc mockMvc;
private BookRepository bookRepository = mock(BookRepository.class);
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new BookController(bookRepository))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
}
@Test
void testGetAllBooks() throws Exception {
when(bookRepository.findAll()).thenReturn(Arrays.asList(
new Book(1L, "Spring in Action", "Craig Walls"),
new Book(2L, "Clean Code", "Robert Martin")
));
mockMvc.perform(get("/api/books"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].title").value("Spring in Action"))
.andExpect(jsonPath("$[1].title").value("Clean Code"));
}
@Test
void testAddBook() throws Exception {
Book newBook = new Book(null, "New Book", "Author");
Book savedBook = new Book(1L, "New Book", "Author");
when(bookRepository.save(any(Book.class))).thenReturn(savedBook);
mockMvc.perform(post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"New Book\",\"author\":\"Author\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/books/1")))
.andExpect(jsonPath("$.id").value(1));
}
@Test
void testGetBookNotFound() throws Exception {
when(bookRepository.findById(1L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/books/1"))
.andExpect(status().isNotFound());
}
// 其他CRUD操作的测试类似...
}
六、standaloneSetup 最佳实践
适用场景:
- 针对单个控制器的单元测试
- 需要快速执行的小型测试
- 不需要完整Spring上下文的测试
优势:
- 测试启动速度快
- 可以精确控制测试环境
- 避免加载不必要的组件
局限性:
- 不会自动加载Spring配置
- 需要手动管理依赖
- 不适用于需要完整Spring上下文的测试
与webAppContextSetup的选择:
- 如果测试需要完整的Spring MVC基础设施(如过滤器、拦截器等),使用
webAppContextSetup
- 如果只需要测试单个控制器的逻辑,使用
standaloneSetup
更高效
- 如果测试需要完整的Spring MVC基础设施(如过滤器、拦截器等),使用
通过以上案例可以看出,standaloneSetup
非常适合针对单个控制器进行隔离测试,特别是当您需要快速验证控制器逻辑而不想加载完整Spring上下文时。