# Spirng Test ## Unit Test ### Introduce 在运行单元测试时,无需实际启动容器,可以通过`mock objects`的方式来独立的对代码进行测试。故而,单元测试通常运行的十分快。 ### Mock Objects spring包含如下专门用于`mock`的包 - Environment - JNDI - Servlet API - Spring Web Reactive #### Environment `org.springframework.mock.env`包中包含`Environment`和`PropertySource`抽象类的实现。在编写针对依赖环境变量代码的测试时,`MockEnvironment`和`MockPropertySource`将会非常有用。 #### JNDI `org.springframework.mock.jndi`包中包含`JNDI SPI`的部分实现,因而,可以建立一个简单的JNDI环境。 #### Servlet API `org.springframework.mock.web`包中包含一整套`servlet api mock object`,这些mock object针对spring web mvc框架使用。 ### Unit Test Support Class spring中包含一系列类来帮助单元测试: - 通用测试组件 - spring mvc测试组件 #### 通用测试组件 `org.springframework.test.util`中包含一系列类用于单元测试和集成测试。 `AopTestUtils`中拥有一系列aop相关的方法。可以通过AopTestUtils中的方法获取隐藏在一层或多层代理下的`target`对象。 `ReflectionTestUtils`中拥有一系列反射相关的方法,可以通过ReflectionTestUtils中的方法修改常量值、访问非public的field或method。通常,ReflectionTestUtils可用于如下场景: - ORM框架中针对protected或private filed的访问 - spring注解(@Autowired、@Inject,@Resource,@PostConstruct)中对于类中非公有成员的访问 `TestSocketUtils`可以查找本地可以连接的TCP端口,通常用于集成测试。 #### spring mvc测试组件 `org.springframework.test.web`包包含了`ModelAndViewAssert`. ## 集成测试 集成测试拥有如下特性: - 通过spring上下文正确注入 - 可以通过JDBC或ORM工具进行数据库访问 集成测试会实际启动spring容器,故而速度要比单元测试慢。 ### 集成测试主要目标 集成测试主要支持如下目标: - 在测试之间管理ioc容器缓存 - 在测试时提供依赖注入 - 在集成测试时提供事务管理 - 在编写集成测试时提供spring相关的类 ### Context Management and Caching spring TestContext framework支持一致导入`ApplicationContext`和`WebApplicationContext`,并针对这些context做缓存。 > 支持对于已导入上下文的缓存是很重要的,因为spring实例化对象时花费的时间会很长。故而,如果针对每个测试的每个test fixture之前,都会加载对象的开销。 默认情况下,一旦`ApplicationContext`导入,那么每个test都会复用导入的上下文。故而,每个test suite都只会有一次导入开销,并且后续测试的执行要快得多。其中,`test suite`代表运行在相同jvm中的所有测试, ### Test Fixtures依赖注入 当TestContext framework导入应用上下文时,其可以针对test class使用依赖注入。并且,可以跨测试场景重复使用应用程序上下文,避免在独立的测试用例之间重复执行fixture设置。 ### 事务管理 TextContext framework默认会为每个test都创建并回滚一个事务。在编写test代码时,可以假定已经存在事务。默认情况下,test方法执行完后事务会被回滚,数据库状态会恢复到执行前的状态。 如果想要令test方法执行后事务被提交,可以使用`@Commit`注解。 ### JDBC Support `org.springframework.test.jdbc`包中包含`JdbcTestUtils`,其中包含了一系列jdbc相关的工具方法。 ## Spring TestContext Framework Spring TestContext Framework提供了通用的、注解驱动的单元测试和集成测试支持,并且该支持和底层的测试框架无关。并且,TestContext Framework的约定大于配置,并可以通过注解来覆盖默认值。 ### Context Management 每个`TestContext`都为其负责的测试类提供了上下文管理和caching支持。测试类实例并不直接访问配置好的`ApplicationContext`,但如果test class实现了`ApplciationContextAware`接口,那么测试类中将包含field指向ApplicationContext的引用。 在使用TestContext Framework时,test class并不需要继承特定类或实现特定接口来配置test class所属的application context,相应的,application context的配置是通过在类级别指定`@ContextConfiguration`来实现的。如果test class没有显式的声明application context resource location或component classes,那么配置好的`ContextLoader`将会决定如何从默认location或默认configuration classes中加载context。 #### Context Configuration with Xml Resource 如果要通过xml配置的方式来导入`ApplicationContext`,可以指定`@ContextConfiguration`中的`location`属性,其是执行xml文件路径的数组。以`/`开头的路径代表classpath,而相对路径则是代表相对class文件的路径。示例如下: ```java @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/app-config.xml" and // "/test-config.xml" in the root of the classpath @ContextConfiguration(locations = {"/app-config.xml", "/test-config.xml"}) class MyTest { // class body... } ``` 如果@ContextConfiguration省略了`location`和`value`属性,那么其默认会探测test class所在的路径。例如`com.example.MyTest`类,其默认会探测`classpath:com/example/MyTest-context.xml`。 #### Context Configuration with Component Classes 为test加载ApplicationContext时,可以通过componet classes来实现。为此,可以为test class指定`@ConfigurationContext`注解,并且在注解的`classes`属性指定一个class数组,其实现如下: ```java @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from AppConfig and TestConfig @ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) class MyTest { // class body... } ``` > 其中,component classes可以指定如下的任何一种对象: > - 有`@Configuration`注解的类 > - 有`@Component`、`@Service`、`@Repository`等注解的类 > - 有`@Bean`注解方法返回的bean对象所属类 > - 其他任何被注册为spring bean对象的类 如果,在指定@ConfigurationContext注解时,省略了`classes`属性,那么TestContext Framework会尝试查找是否存在默认的classes。`AnnotationConfigContextLoader`和`AnnotationConfigWebContextLoader`会查找Test Class中所有的静态内部类,并判断其是否符合configuration class需求。 示例如下所示: ```java @SpringJUnitConfig // ApplicationContext will be loaded from the static nested Config class class OrderServiceTest { @Configuration static class Config { // this bean will be injected into the OrderServiceTest class @Bean OrderService orderService() { OrderService orderService = new OrderServiceImpl(); // set properties, etc. return orderService; } } @Autowired OrderService orderService; @Test void testOrderService() { // test the orderService } } ``` #### Context Configuration继承 `@ContextConfiguration`支持从父类继承componet classes、resource locations、context initializers。可以指定`inheritLocations`和`inheritInitializers`来决定是否继承父类componet classes、resource locations、context initializers,两属性默认值为`true`。 示例如下: ```java @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/base-config.xml" // in the root of the classpath @ContextConfiguration("/base-config.xml") class BaseTest { // class body... } // ApplicationContext will be loaded from "/base-config.xml" and // "/extended-config.xml" in the root of the classpath @ContextConfiguration("/extended-config.xml") class ExtendedTest extends BaseTest { // class body... } ``` ```java // ApplicationContext will be loaded from BaseConfig @SpringJUnitConfig(BaseConfig.class) class BaseTest { // class body... } // ApplicationContext will be loaded from BaseConfig and ExtendedConfig @SpringJUnitConfig(ExtendedConfig.class) class ExtendedTest extends BaseTest { // class body... } ``` ```java // ApplicationContext will be initialized by BaseInitializer @SpringJUnitConfig(initializers = BaseInitializer.class) class BaseTest { // class body... } // ApplicationContext will be initialized by BaseInitializer // and ExtendedInitializer @SpringJUnitConfig(initializers = ExtendedInitializer.class) class ExtendedTest extends BaseTest { // class body... } ``` #### Context Configuration with Environment Profiles 可以通过为test class指定`@ActiveProfiles`注解来指定激活的profiles。 ```java @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "classpath:/app-config.xml" @ContextConfiguration("/app-config.xml") @ActiveProfiles("dev") class TransferServiceTest { @Autowired TransferService transferService; @Test void testTransferService() { // test the transferService } } ``` `@ActiveProfiles`同样支持`inheritProfiles`属性,默认为true,可以关闭: ```java // "dev" profile overridden with "production" @ActiveProfiles(profiles = "production", inheritProfiles = false) class ProductionTransferServiceTest extends AbstractIntegrationTest { // test body } ``` #### Context Configuration with TestPropertySource 可以通过在test class上声明`@TestPropertySource`注解来指定test properties文件位置。 `@TestPropertySource`可以指定lcoations和value属性,默认情况下支持xml和properties类型的文件,也可以通过`factory`指定一个`PropertySourceFactory`来支持不同格式的文件,例如`yaml`等。 每个path都会被解释为spring resource。 示例如下所示: ```java @ContextConfiguration @TestPropertySource("/test.properties") class MyIntegrationTests { // class body... } ``` ```java @ContextConfiguration @TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) class MyIntegrationTests { // class body... } ``` 如果在声明`@TestPropertySource`时没有指定locations和value属性的值,其会在test class所在路径去查找。例如`com.example.MyTest`其默认会去查找`classpath:com/example/MyTest.properties`. ##### 优先级 test properties的优先级将会比定义在操作系统environment、java system properties、应用程序手动添加的propertySource高。 ##### 继承和覆盖 `@TestPropertySource`注解也支持`inheritLocations`和`inheritProperties`属性,可以从父类中继承resource location和inline properties。默认情况下,两属性的值为true。 示例如下: ```java @TestPropertySource("base.properties") @ContextConfiguration class BaseTest { // ... } @TestPropertySource("extended.properties") @ContextConfiguration class ExtendedTest extends BaseTest { // ... } ``` ```java @TestPropertySource(properties = "key1 = value1") @ContextConfiguration class BaseTest { // ... } @TestPropertySource(properties = "key2 = value2") @ContextConfiguration class ExtendedTest extends BaseTest { // ... } ``` #### Loading WebApplicationContext 如果要导入WebApplicationContext,可以使用`@WebAppConfiguration`. 在使用`@WebAppConfiguration`注解之后,还可以自由使用`@ConfigurationContext`等类。 ```java @ExtendWith(SpringExtension.class) // defaults to "file:src/main/webapp" @WebAppConfiguration // detects "WacTests-context.xml" in the same package // or static nested @Configuration classes @ContextConfiguration class WacTests { //... } ``` #### Context Caching 一旦TestContext framework为test导入了Application Context,那么context将会被缓存,并且为之后相同test suite中所有有相同context configuration的test复用。 一个`ApplicationContext`可以被其创建时的参数组唯一标识,创建ApplicationContext所用到的参数将会产生一个唯一的key,该key将会作为context缓存的key。context cache key将会用到如下参数: - `locations`(`@ContextConfiguration`) - `classes`(`@ContextConfiguration`) - `contextInitializerClasses`(`@ContextConfiguration`) - `contextCustomizers`(`ContextCustomizerFactory`) - `contextLoader`(`@ContextConfiguration`) - `parent`(`@ContextHierarchy`) - `activeProfiles`(`@ActiveProfiles`) - `propertySourceDescriptors`(`@TestPropertySource`) - `propertySourceProperties`(`@TestPropertySource`) - `resourceBasePath`(来源于`@WebAppConfiguration`) 如果两个test classes对应的key相同,那么它们将共用application context。 该context cache默认大小上限为32,到到达上限后,将采用`LRU`来淘汰被缓存的context。 ### Test Fixture Dependency Injection test class中的依赖对象将会自动从application context中注入,可以使用setter注入、field注入。 示例如下所示: ```java @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type @Autowired HibernateTitleRepository titleRepository; @Test void findById() { Title title = titleRepository.findById(new Long(10)); assertNotNull(title); } } ``` setter注入示例如下所示: ```java @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type HibernateTitleRepository titleRepository; @Autowired void setTitleRepository(HibernateTitleRepository titleRepository) { this.titleRepository = titleRepository; } @Test void findById() { Title title = titleRepository.findById(new Long(10)); assertNotNull(title); } } ``` ### 事务管理 为了启用事务支持,需要在`ApplicationContext`中配置`PlatformTransactionManager`bean,此外,必须在test方法上声明`@Transactional`注解。 test-managed transaction是由`TransactionalTestExecutionListener`管理的,spring-managed transaction和applicaion-managed transaction都会加入到test-managed transaction,但是当指定了spring-managed transaction和applicaion-managed transaction的传播行为时,可能会在独立事务中运行。 > 当为test method指定@Transactional注解时,会导致test方法在事务中运行,并且默认会在事务完成后自动回滚。若test method没有指定@Transactional注解,那么test method将不会在事务中运行。test lifecycle method并不支持添加@Transactional注解。 示例如下所示: ```java @SpringJUnitConfig(TestConfig.class) @Transactional class HibernateUserRepositoryTests { @Autowired HibernateUserRepository repository; @Autowired SessionFactory sessionFactory; JdbcTemplate jdbcTemplate; @Autowired void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Test void createUser() { // track initial state in test database: final int count = countRowsInTable("user"); User user = new User(...); repository.save(user); // Manual flush is required to avoid false positive in test sessionFactory.getCurrentSession().flush(); assertNumUsers(count + 1); } private int countRowsInTable(String tableName) { return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); } private void assertNumUsers(int expected) { assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")); } } ``` #### 事务提交和回滚行为 默认情况下,事务在test方法执行完后自动会回滚,但是,事务执行完后的行为可以通过`@Commit`或`@Rollback`注解来控制。 如下是所有事务相关注解演示: ```java @SpringJUnitConfig @Transactional(transactionManager = "txMgr") @Commit class FictitiousTransactionalTest { @BeforeTransaction void verifyInitialDatabaseState() { // logic to verify the initial state before a transaction is started } @BeforeEach void setUpTestDataWithinTransaction() { // set up test data within the transaction } @Test // overrides the class-level @Commit setting @Rollback void modifyDatabaseWithinTransaction() { // logic which uses the test data and modifies database state } @AfterEach void tearDownWithinTransaction() { // run "tear down" logic within the transaction } @AfterTransaction void verifyFinalDatabaseState() { // logic to verify the final state after transaction has rolled back } } ``` ### test request spring mvc中的测试示例如下所示: ```java @SpringJUnitWebConfig class RequestScopedBeanTests { @Autowired UserService userService; @Autowired MockHttpServletRequest request; @Test void requestScope() { request.setParameter("user", "enigma"); request.setParameter("pswd", "$pr!ng"); LoginResults results = userService.loginUser(); // assert results } } ``` ## spring boot test spring boot提供了`@SpringBootTest`注解,可以作为`@ContextConfiguration`注解的代替。该类实际是通过spring boot项目的启动类来创建了一个ApplicationContext。 默认情况下,`@SpringBootTest`并不会启动server,可以使用注解的`webEnvironment`来对运行环境重新定义,该属性可选值如下: - `MOCK`(默认):将会导入一个ApplicationContext并且提供一个mock web environment。内置的server将不会被启动。如果classpath中没有web环境,那么其将会只创建一个非web的ApplicationContext。其可以和基于mock的`@AutoConfigureMockMvc`与`@AutoConfigureWebTestClient`一起使用 - `RANDOM_PORT`:导入`WebServerApplicationContext`并提供一个真实的web环境,内部server启动,并监听随机端口 - `DEFINED_PORT`导入`WebServerApplicationContext`并提供一个真实的web环境,内部server启动,监听指定端口(默认8080) - `NONE`:通过SpringApplication导入ApplicationContext,但是不提供任何web环境 ### @TestConfiguration 如果想要为测试新建Configuration顶级类,不应该使用`@Configuration`注解,这样会被@SpringBootApplciation或@ComponentScan扫描到,应该使用`@TestConfiguration`注解,并将类修改为嵌套类。 如果@TestConfiguration为顶层类,那么该类不会被注册,应该显式import: ```java @RunWith(SpringRunner.class) @SpringBootTest @Import(MyTestsConfiguration.class) public class MyTests { @Test public void exampleTest() { ... } } ``` ### testing with mock environment ```java import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class MockMvcExampleTests { @Autowired private MockMvc mvc; @Test public void exampleTest() throws Exception { this.mvc.perform(get("/")).andExpect(status().isOk()) .andExpect(content().string("Hello World")); } } ``` ### slice 通常,测试时只需要部分configuration,例如只需要service层而不需要web层。 `spring-boot-test-autoconfigure`模块提供了一系列注解,可以进行slice操作。该模块提供了`@…​Test`格式的注解用于导入ApplicationContext,并提供了一个或多个`@AutoConfigure…​`来自定义自动装配设置。