540 lines
20 KiB
Markdown
540 lines
20 KiB
Markdown
# 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…`来自定义自动装配设置。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|