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

20 KiB
Raw Blame History

Spirng Test

Unit Test

Introduce

在运行单元测试时,无需实际启动容器,可以通过mock objects的方式来独立的对代码进行测试。故而,单元测试通常运行的十分快。

Mock Objects

spring包含如下专门用于mock的包

  • Environment
  • JNDI
  • Servlet API
  • Spring Web Reactive

Environment

org.springframework.mock.env包中包含EnvironmentPropertySource抽象类的实现。在编写针对依赖环境变量代码的测试时,MockEnvironmentMockPropertySource将会非常有用。

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支持一致导入ApplicationContextWebApplicationContext并针对这些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省略了locationvalue属性那么其默认会探测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。AnnotationConfigContextLoaderAnnotationConfigWebContextLoader会查找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。可以指定inheritLocationsinheritInitializers来决定是否继承父类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注解也支持inheritLocationsinheritProperties属性可以从父类中继承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…来自定义自动装配设置。