20 KiB
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文件的路径。示例如下:
@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数组,其实现如下:
@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需求。
示例如下所示:
@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。
示例如下:
@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...
}
// 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...
}
// 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。
@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,可以关闭:
// "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。
示例如下所示:
@ContextConfiguration
@TestPropertySource("/test.properties")
class MyIntegrationTests {
// class body...
}
@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。
示例如下:
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
Loading WebApplicationContext
如果要导入WebApplicationContext,可以使用@WebAppConfiguration.
在使用@WebAppConfiguration注解之后,还可以自由使用@ConfigurationContext等类。
@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注入。
示例如下所示:
@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注入示例如下所示:
@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中配置PlatformTransactionManagerbean,此外,必须在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注解。
示例如下所示:
@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注解来控制。
如下是所有事务相关注解演示:
@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中的测试示例如下所示:
@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:
@RunWith(SpringRunner.class)
@SpringBootTest
@Import(MyTestsConfiguration.class)
public class MyTests {
@Test
public void exampleTest() {
...
}
}
testing with mock environment
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…来自定义自动装配设置。