Files
rikako-note/spring/spring test/SpringTest.md
2024-06-02 20:57:23 +08:00

540 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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