Spring 框架提供了一个专门的测试模块( spring-test ),用于应用程序的集成测试。 在 Spring Boot 中,你可以通过 spring-boot-starter-test 启动器快速开启和使用它。

# pom.xml


1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

1. Junit 测试

当你的单元测试代码不需要用到 Spring Boot 功能,而只是一个简单的测试时,你可以直接编写你的 Junit 测试代码:

1
2
3
4
5
6
7
8
public class SimpleJunitTest {
@Test
public void testSayHi() {
System.out.println("Hi Junit.");
}
}

2. Spring Boot 测试

当你的集成测试代码需要用到 Spring Boot 功能时,你可以使用 @SpringBootTest 注解。
该注解是普通的 Spring 项目(非 Spring Boot 项目)中编写集成测试代码所使用的 @ContextConfiguration 注解的替代品。其作用是用于确定如何装载 Spring 应用程序的上下文资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class BeanInjectTest {
@Autowired
private HelloService helloService;
@Test
public void testSayHi() {
System.out.println(helloService.sayHi());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class HelloService {
public String sayHi() {
return "--- Hi ---";
}
public String sayHello() {
return "--- Hello ---";
}
}

当运行 Spring Boot 应用程序测试时,它会自动的从当前测试类所在的包起一层一层向上搜索,直到找到一个 @SpringBootApplication @SpringBootConfiguration 注释类为止。以此来确定如何装载 Spring 应用程序的上下文资源。只要你以合理的方式组织你的代码,你项目的主配置通常是可以被发现的。本示例项目的部分文件结构图为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring-boot-testing-sample
\__ src
\__ main
: \__ java
: \__ org
: \__ fanlychie
: |__ Application.java
: \__ service
: |__ HelloService.java
\__ test
\__ java
\__ org
\__ fanlychie
\__ test
|__ BeanInjectTest.java

其中,主配置启动类的代码为:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

如果搜索算法搜索不到你项目的主配置文件,将报出异常:

java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=…) with your test

解决办法是,按 Spring Boot 的约定重新组织你的代码结构,或者手工指定你要装载的主配置文件:

1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {YourApplication.class})
public class BeanInjectTest {
// ...
}

基于 Spring 环境的 Junit 集成测试还需要使用 @RunWith(SpringJUnit4ClassRunner.class) 注解,该注解能够改变 Junit 并让其运行在 Spring 的测试环境,以得到 Spring 测试环境的上下文支持。否则,在 Junit 测试中,Bean 的自动装配等注解将不起作用。但由于 SpringJUnit4ClassRunner 不方便记忆,Spring 4.3 起提供了一个等同于 SpringJUnit4ClassRunner 的类 SpringRunner,因此可以简写成: @RunWith(SpringRunner.class)

3. Spring MVC 测试

当你想对 Spring MVC 控制器编写单元测试代码时,可以使用 @WebMvcTest 注解。它提供了自配置的 MockMvc,可以不需要完整启动 HTTP 服务器就可以快速测试 MVC 控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class HelloControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void testHello() throws Exception {
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andDo(print());
}
}
1
2
3
4
5
6
7
8
9
10
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(ModelMap model) {
model.put("message", "Hello Page");
return "hello";
}
}

使用 @WebMvcTest 注解时,只有一部分的 Bean 能够被扫描得到,它们分别是:

  • @Controller
  • @ControllerAdvice
  • @JsonComponent
  • Filter
  • WebMvcConfigurer
  • HandlerMethodArgumentResolver
  • 其他常规的 @Component (包括 @Service @Repository 等)Bean 则不会被加载到 Spring 测试环境上下文中。

    如果测试的 MVC 控制器中需要 @Component Bean 的参与,你可以使用 @MockBean 注解来协助完成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import static org.mockito.BDDMockito.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    @RunWith(SpringRunner.class)
    @WebMvcTest(HelloController.class)
    public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private HelloService helloService;
    @Test
    public void testSayHi() throws Exception {
    // 模拟 HelloService.sayHi() 调用, 返回 "=== Hi ==="
    when(helloService.sayHi()).thenReturn("=== Hi ===");
    mvc.perform(get("/hello/sayHi"))
    .andExpect(status().isOk())
    .andDo(print());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Controller
    public class HelloController {
    @Autowired
    private HelloService helloService;
    @GetMapping("/hello/sayHi")
    public String sayHi(ModelMap model) {
    model.put("message", helloService.sayHi());
    return "hello";
    }
    }

    4. Spring Boot Web 测试

    当你想启动一个完整的 HTTP 服务器对 Spring Boot 的 Web 应用编写测试代码时,可以使用 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 注解开启一个随机的可用端口。Spring Boot 针对 REST 调用的测试提供了一个 TestRestTemplate 模板,它可以解析链接服务器的相对地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    public class ApplicationTest {
    @Autowired
    private TestRestTemplate restTemplate;
    @Test
    public void testSayHello() {
    Map<String, Object> result = restTemplate.getForObject("/hello/sayHello", Map.class);
    System.out.println(result.get("message"));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Controller
    public class HelloController {
    @Autowired
    private HelloService helloService;
    @GetMapping("/hello/sayHello")
    public @ResponseBody Object helloInfo() {
    Map<String, Object> map = new HashMap<>();
    map.put("message", helloService.sayHello());
    return map;
    }
    }

    5. Spring Data JPA 测试

    当你想对 Spring Data JPA 应用进行单元测试时,你可以使用 @DataJpaTest 注解。并且在进行 JPA 测试时,你可以选择使用内存数据库还是真实的数据库测试。

    5.1 内存数据库测试

    默认情况下, @DataJpaTest 使用的是内存数据库进行测试,你无需配置和启用真实的数据库。只需要在 pom.xml 配置文件中声明如下依赖即可:

    # pom.xml


    1
    2
    3
    4
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    </dependency>

    @DataJpaTest 注解它只扫描 @Entity Bean 和装配 Spring Data JPA 存储库,其他常规的 @Component (包括 @Service @Repository 等)Bean 则不会被加载到 Spring 测试环境上下文。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class UserRepositoryInMemoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testSave() {
    User user = new User();
    user.setName("fanlychie");
    userRepository.save(user);
    System.out.println("====================================");
    System.out.println(userRepository.findAll());
    System.out.println("====================================");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Entity(name = "User")
    public class User {
    @Id
    @GeneratedValue(generator = "uuidGenerator")
    @GenericGenerator(name = "uuidGenerator", strategy = "uuid")
    private String id;
    private String name;
    // getters and setters
    }
    1
    2
    3
    public interface UserRepository extends JpaRepository<User, String> {
    }

    5.2 真实数据库测试

    如果你希望使用真实的数据库做测试,你可以使用 @AutoConfigureTestDatabase(replace = Replace.NONE) 注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = Replace.NONE)
    public class UserRepositoryMySQLTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testSave() {
    User user = new User();
    user.setName("fanlychie");
    userRepository.save(user);
    System.out.println("====================================");
    System.out.println(userRepository.findAll());
    System.out.println("====================================");
    }
    }

    replace = Replace.NONE 的作用是告知 Spring Boot 不要替换应用程序默认的数据源。

    # src/main.resources/application.yml


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    datasource:
    url: jdbc:mysql://127.0.0.1/test
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    tomcat:
    default-auto-commit: true
    jpa:
    hibernate:
    ddl-auto: update

    5.3 事务控制

    默认情况下,在每个 JPA 测试结束时,事务会发生回滚。这在一定程度上可以防止测试数据污染数据库。如果你不希望事务发生回滚,你可以使用 @Rollback(false) 注解,该注解可以标注在类级别做全局的控制,也可以标注在某个特定不需要执行事务回滚的方法级别上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = Replace.NONE)
    public class UserRepositoryMySQLTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    @Rollback(false)
    public void testSave() {
    User user = new User();
    user.setName("fanlychie");
    userRepository.save(user);
    System.out.println("====================================");
    System.out.println(userRepository.findAll());
    System.out.println("====================================");
    }
    }

    另外,你也可以使用 @Transactional 注解对事务进行控制。该注解可以标注在类级别做全局的控制,也可以标注在某个特定的方法级别上。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = Replace.NONE)
    public class UserRepositoryMySQLTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    @Transactional(readOnly = true)
    public void testSelect() {
    System.out.println("====================================");
    System.out.println(userRepository.findAll());
    System.out.println("====================================");
    }
    }