集成测试
SpringBootTest 集成测试
利用 @SpringBootTest 注解,Spring Boot 提供了一种方便的方法来启动要在测试中使用的应用程序上下文。默认情况下,@SpringBootTest 开始在测试类的当前程序包中搜索,然后向上搜索程序包结构,查找带有 @SpringBootConfiguration 注释的类,然后从该类中读取配置,以创建应用程序上下文。此类通常是我们的主要应用程序类,因为 @SpringBootApplication 注解包含 @SpringBootConfiguration 注解。然后它创建一个与在生产环境中启动的应用程序上下文非常相似的应用程序上下文。
因为我们拥有完整的应用程序上下文,包括 Web 控制器,Spring Data 存储库和数据源,所以 @SpringBootTest 对于通过应用程序所有层的集成测试非常方便:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Test
void registrationWorksThroughAllLayers() throws Exception {
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
mockMvc
.perform(
post("/forums/{forumId}/register", 42L)
.contentType("application/json")
.param("sendWelcomeMail", "true")
.content(objectMapper.writeValueAsString(user))
)
.andExpect(status().isOk());
UserEntity userEntity = userRepository.findByName("Zaphod");
assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
}
}
自定义 Application Context
一个相对完善的集成测试用例如下:
@SpringBootTest(
SpringBootTest.WebEnvironment.MOCK,
classes = Application.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@Autowired
private EmployeeRepository repository;
// write test cases here
}
我们可以使用 @SpringBootTest 的 webEnvironment 属性来配置我们的运行时环境。我们在这里使用 WebEnvironment.MOCK,这样容器将在模拟 servlet 环境中运行。我们可以使用 @TestPropertySource 批注来配置特定于我们的测试的属性文件的位置。请注意,使用 @TestPropertySource 加载的属性文件将覆盖现有的 application.properties 文件。application-integrationtest.properties 包含配置持久性存储的详细信息:
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
如果要针对 MySQL 运行集成测试,可以在属性文件中更改以上值。
自定义属性
通常,在测试中,有必要将一些配置属性设置为与生产设置中的值不同的值:
@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {
@Value("${foo}")
String foo;
@Test
void test() {
assertThat(foo).isEqualTo("bar");
}
}
如果我们的许多测试需要相同的属性集,则可以创建配置文件 application-<profile>.properties
或application-<profile>.yml
,并通过激活某个配置文件从该文件中加载属性:
// # application-test.yml
// foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {
@Value("${foo}")
String foo;
@Test
void test() {
assertThat(foo).isEqualTo("bar");
}
}
自定义整套属性的另一种方法是使用 @TestPropertySource 批注:
// # src/test/resources/foo.properties
// foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {
@Value("${foo}")
String foo;
@Test
void test() {
assertThat(foo).isEqualTo("bar");
}
}
自定义 Bean
如果我们只想测试应用程序的某个部分而不是测试从传入请求到数据库的整个路径,则可以使用@MockBean 替换应用程序上下文中的某些 bean:
@SpringBootTest
class MockBeanTest {
@MockBean
private UserRepository userRepository;
@Autowired
private RegisterUseCase registerUseCase;
@Test
void testRegister() {
// given
User user = new User("Zaphod", "zaphod@galaxy.net");
boolean sendWelcomeMail = true;
given(userRepository.save(any(UserEntity.class)))
.willReturn(userEntity(1L));
// when
Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
// then
assertThat(userId).isEqualTo(1L);
}
}
在这种情况下,我们用模拟代替了 UserRepository Bean,我们使用 Mockito 的 when 方法指定了该模拟的预期行为,以便测试使用此存储库的类。如果某些 Bean 没有包含在默认应用程序上下文中,但是我们需要在测试中使用它们,则可以使用 @Import 批注导入它们:
package other.namespace;
@Component
public class Foo {}
@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {
@Autowired
Foo foo;
@Test
void test() {
assertThat(foo).isNotNull();
}
}
默认情况下,Spring Boot 应用程序会在其包和子包中包含它找到的所有组件,因此通常仅在我们要包括其他包中的 Bean 时才需要。
自定义 SpringBootApplication
我们甚至可以创建一个完整的自定义 Spring Boot 应用程序以在测试中启动。如果此应用程序类与真实应用程序类位于同一程序包中,但在测试源而非生产源中,则 @SpringBootTest 将在实际应用程序类之前找到它,并从此应用程序中加载应用程序上下文。
@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {}
但是在执行此操作时,我们正在测试可能与生产环境完全不同的应用程序上下文,这种做法反而不值得推荐,我们更应该去以条件方式控制某个 Bean 是否被注册。假设我们在应用程序类上使用 @EnableScheduling 批注。每次启动应用程序上下文时(即使在测试中),所有 @Scheduled 作业都将启动,并且可能与我们的测试冲突。我们通常不希望作业在测试中运行,因此我们可以创建没有 @EnabledScheduling 批注的第二个应用程序类,并在测试中使用它。更好的解决方案是创建一个可通过属性切换的配置类:
@Configuration
@EnableScheduling
@ConditionalOnProperty(
name = "wx.scheduling.enabled",
havingValue = "true",
matchIfMissing = true
)
public class SchedulingConfiguration {}
@SpringBootTest(properties = "wx.scheduling.enabled=false")
class SchedulingTest {
@Autowired(required = false)
private SchedulingConfiguration schedulingConfiguration;
@Test
void test() {
assertThat(schedulingConfiguration).isNull();
}
}
数据库的集成测试
Mybatis 内存测试
我们还是建议尽可能地使用内存数据库进行测试,以下就是使用 HSQLDB 测试的用例:
@ExtendWith(SpringExtension.class)
@SpringBootTest(
classes = { ApplicationTunnelTest.TestConfig.class, ApplicationTunnel.class }
)
class ApplicationTunnelTest {
@Autowired
private ApplicationTunnel applicationTunnel;
@Test
void testList() {
Assertions.assertNotEquals(0, applicationTunnel.list().size());
}
@Configuration
@MapperScan(basePackageClasses = { ApplicationMapper.class })
@EnableTransactionManagement
public static class TestConfig extends AbstractHsqlMyBatisDbConfig {
@Override
protected void configDataSourceBuilder(EmbeddedDatabaseBuilder builder) {
builder
.setName("admin_wx_application")
.addScript("classpath:db/schema/hsql/create_admin_schema.sql")
.addScript("classpath:db/data/common/create_admin_system_data.sql")
.addScript("classpath:db/data/common/create_admin_test_data.sql");
}
}
}