Skip to content

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 最佳实践

  1. 适用场景

    • 针对单个控制器的单元测试
    • 需要快速执行的小型测试
    • 不需要完整Spring上下文的测试
  2. 优势

    • 测试启动速度快
    • 可以精确控制测试环境
    • 避免加载不必要的组件
  3. 局限性

    • 不会自动加载Spring配置
    • 需要手动管理依赖
    • 不适用于需要完整Spring上下文的测试
  4. 与webAppContextSetup的选择

    • 如果测试需要完整的Spring MVC基础设施(如过滤器、拦截器等),使用webAppContextSetup
    • 如果只需要测试单个控制器的逻辑,使用standaloneSetup更高效

通过以上案例可以看出,standaloneSetup非常适合针对单个控制器进行隔离测试,特别是当您需要快速验证控制器逻辑而不想加载完整Spring上下文时。