Spring&Spring Boot Testing工具提供了一些方便测试的Annotation,本文会对其中的一些做一些讲解。
@TestPropertySource可以用来覆盖掉来自于系统环境变量,Java的系统属性,@PropertySource的属性。
同时@TestPropertySource(properties=…)优先级高于@TestPropertySource(locations=…)。
利用它我们可以很方便的在测试代码里微调,模拟配置(比如修改操作系统目录分隔符,数据源等)。
我们先使用@PropertySource将一个外部属性文件加载进来,PropertySourceConfig:
@Configuration @PropertySource(“ classpath:me / chanjar / annotation / testps / ex1 / property-source.properties ”) public class PropertySourceConfig { }
file: property-source.properties foo=abc
然后我们用@TestPropertySource覆盖了这个特性:
TestPropertySource(properties = { “ foo = xyz ” ...
最后我们测试了是否覆盖成功(结果是成功的):
@Test public void testOverridePropertySource(){ 的assertEquals(环境。的getProperty( “ FOO ”), “ XYZ ”); }
同时我们还对@TestPropertySource做了一些其他的测试,具体情况你可以自己观察。为了方便你观察@TestPropertySource对系统环境变量和Java的系统属性的覆盖效果,我们在一开始打印出了它们的值。
源代码TestPropertyTest:
@ContextConfiguration(类 = PropertySourceConfig 。类) @TestPropertySource( 属性 = { “富= XYZ ”,“巴= UVW ”,“ PATH = AAA ”,“ java.runtime.name = BBB ” }, 位置 = “类路径:我/chanjar/annotation/testps/ex1/test-property-source.properties “ ) 公共 类 TestPropertyTest 扩展 AbstractTestNGSpringContextTests 实现 EnvironmentAware { 私人 环境环境; @覆盖 公共 无效 setEnvironment(环境 环境){ 此。环境=环境; Map < String,Object > systemEnvironment =((ConfigurableEnvironment)环境)。getSystemEnvironment(); 系统。出去。println(“ ===系统环境=== ”); 系统。出去。的println(getMapString(systemEnvironment)); 系统。出去。的println(); 系统。出去。println(“ === Java系统属性=== ”); Map < String,Object > systemProperties =((ConfigurableEnvironment)环境)。getSystemProperties(); 系统。出去。的println(getMapString(systemProperties)); } @Test public void testOverridePropertySource(){ 的assertEquals(环境。的getProperty( “ FOO ”), “ XYZ ”); } @Test public void testOverrideSystemEnvironment(){ 的assertEquals(环境。的getProperty( “ PATH ”), “ AAA ”); } @Test public void testOverrideJavaSystemProperties(){ 的assertEquals(环境。的getProperty( “ java.runtime.name ”), “ BBB ”); } @Test public void testInlineTestPropertyOverrideResourceLocationTestProperty(){ 的assertEquals(环境。的getProperty( “条”), “ UVW ”); } private String getMapString(Map < String,Object > map){ return String 。加入(“ / n ”, 地图。keySet()。stream()。地图(K - > ķ + “ = ” +地图。得到(k))的。收集(toList()) ); } }
@TestPropertySource也可以和@SpringBootTest一起使用。
源代码见TestPropertyTest:
@SpringBootTest(类 = PropertySourceConfig 。类) @TestPropertySource( 属性 = { “富= XYZ ”,“巴= UVW ”,“ PATH = AAA ”,“ java.runtime.name = BBB ” }, 位置 = “类路径:我/chanjar/annotation/testps/ex1/test-property-source.properties “ ) 公共 类 TestPropertyTest 扩展 AbstractTestNGSpringContextTests 实现 EnvironmentAware { // ..
@ActiveProfiles可以用来在测试的时候启用某些资料的豆本章节的测试代码使用了下面的这个配置:
@Configuration public class Config { @Bean @Profile(“ dev ”) public Foo fooDev(){ return new Foo(“ dev ”); } @Bean @Profile(“ product ”) public Foo fooProduct(){ return new Foo(“ product ”); } @Bean @Profile(“ default ”) public Foo fooDefault(){ return new Foo(“ default ”); } @Bean public bar bar(){ return new bar(“ no profile ”); } }
在没有@ActiveProfiles的时候,外形=默认和没有设定个人资料的豆会被加载到。
源代码ActiveProfileTest:
@ContextConfiguration(类 = 配置。类) 公共 类 ActiveProfileTest 延伸 AbstractTestNGSpringContextTests { @Autowired 私人 Foo foo; @Autowired 私人 酒吧 ; @Test public void test(){ 的assertEquals(FOO 。的getName(), “默认”); 的assertEquals(巴。的getName(), “无简档”); } }
当使用了@ActiveProfiles的时候,轮廓匹配的和没有设定个人资料的豆会被加载到。
源代码ActiveProfileTest:
@ContextConfiguration(类 = 配置。类) [ @ActiveProfiles] [doc-active-profiles](“ product ”) public class ActiveProfileTest extends AbstractTestNGSpringContextTests { @Autowired 私人 Foo foo; @Autowired 私人 酒吧 ; @Test public void test(){ 的assertEquals(FOO 。的getName(), “产品”); 的assertEquals(巴。的getName(), “无简档”); } }
在没有@ActiveProfiles的时候,外形=默认和没有设定个人资料的豆会被加载到。
当使用了@ActiveProfiles的时候,轮廓匹配的和没有设定个人资料的豆会被加载到。
@ActiveProfiles同样也可以和@SpringBootTest配合使用,这里就不举例说明了。
@JsonTest是Spring Boot提供的方便测试JSON序列化反序列化的测试工具,在Spring Boot的文档中有一些介绍。
需要注意的是@JsonTest需要Jackson的ObjectMapper,事实上如果你的Spring Boot项目添加了spring-web的Maven依赖,JacksonAutoConfiguration就会自动为你配置一个:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency>
这里没有提供关于日期时间的例子,关于这个比较复杂,可以看我的另一篇文章: Spring Boot Jackson对于日期时间类型处理的例子 。
源代码见SimpleJsonTest:
@SpringBootTest(classes = SimpleJsonTest.class) @JsonTest public class SimpleJsonTest extends AbstractTestNGSpringContextTests { @Autowired private JacksonTester<Foo> json; @Test public void testSerialize() throws Exception { Foo details = new Foo("Honda", 12); // 使用通包下的json文件测试结果是否正确 assertThat(this.json.write(details)).isEqualToJson("expected.json"); // 或者使用基于JSON path的校验 assertThat(this.json.write(details)).hasJsonPathStringValue("@.name"); assertThat(this.json.write(details)).extractingJsonPathStringValue("@.name").isEqualTo("Honda"); assertThat(this.json.write(details)).hasJsonPathNumberValue("@.age"); assertThat(this.json.write(details)).extractingJsonPathNumberValue("@.age").isEqualTo(12); } @Test public void testDeserialize() throws Exception { String content = "{/"name/":/"Ford/",/"age/":13}"; Foo actual = this.json.parseObject(content); assertThat(actual).isEqualTo(new Foo("Ford", 13)); assertThat(actual.getName()).isEqualTo("Ford"); assertThat(actual.getAge()).isEqualTo(13); } }
@JsonTest可以用来测试@JsonComponent。
这个例子里使用了自定义的@JsonComponent FooJsonComponent:
@JsonComponent public class FooJsonComponent { public static class Serializer extends JsonSerializer<Foo> { @Override public void serialize(Foo value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { // ... } } public static class Deserializer extends JsonDeserializer<Foo> { @Override public Foo deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { // ... } } }
测试代码JsonComponentJsonTest:
@SpringBootTest(classes = { JsonComponentJacksonTest.class, FooJsonComponent.class }) @JsonTest public class JsonComponentJacksonTest extends AbstractTestNGSpringContextTests { @Autowired private JacksonTester<Foo> json; @Test public void testSerialize() throws Exception { Foo details = new Foo("Honda", 12); assertThat(this.json.write(details).getJson()).isEqualTo("/"name=Honda,age=12/""); } @Test public void testDeserialize() throws Exception { String content = "/"name=Ford,age=13/""; Foo actual = this.json.parseObject(content); assertThat(actual).isEqualTo(new Foo("Ford", 13)); assertThat(actual.getName()).isEqualTo("Ford"); assertThat(actual.getAge()).isEqualTo(13); } }
事实上@JsonTest也可以配合@ContextConfiguration一起使用。
源代码见ThinJsonTest:
@JsonTest @ContextConfiguration(classes = JsonTest.class) public class ThinJsonTest extends AbstractTestNGSpringContextTests { @Autowired private JacksonTester<Foo> json; @Test public void testSerialize() throws Exception { // ... } @Test public void testDeserialize() throws Exception { // ... } }
在 Spring、Spring Boot 和 TestNG 测试指南 ( 1 ) 提到:
除了单元测试(不需要初始化ApplicationContext的测试)外,尽量将测试配置和生产配置保持一致。比如如果生产配置里启用了AutoConfiguration,那么测试配置也应该启用。因为只有这样才能够在测试环境下发现生产环境的问题,也避免出现一些因为配置不同导致的奇怪问题。
那么当我们想在测试代码里关闭Auto Configuration如何处理?
方法1:提供另一套测试配置
方法2:使用@OverrideAutoConfiguration
方法1虽然能够很好的解决问题,但是比较麻烦。而方法2则能够不改变原有配置、不提供新的配置的情况下,就能够关闭Auto Configuration。
在本章节的例子里,我们自己做了一个Auto Configuration类,AutoConfigurationEnableLogger:
@Configuration public class AutoConfigurationEnableLogger { private static final Logger LOGGER = LoggerFactory.getLogger(AutoConfigurationEnableLogger.class); public AutoConfigurationEnableLogger() { LOGGER.info("Auto Configuration Enabled"); } }
并且在META-INF/spring.factories里注册了它:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=/ me.chanjar.annotation.overrideac.AutoConfigurationEnableLogger
这样一来,只要Spring Boot启动了Auto Configuration就会打印出日志:
2017-08-24 16:44:52.789 INFO 13212 --- [ main] m.c.a.o.AutoConfigurationEnableLogger : Auto Configuration Enabled
源代码见BootTest:
@SpringBootTest @SpringBootApplication public class BootTest extends AbstractTestNGSpringContextTests { @Test public void testName() throws Exception { } }
查看输出的日志,会发现Auto Configuration已经启用。
然后我们用@OverrideAutoConfiguration关闭了Auto Configuration。
源代码见BootTest:
@SpringBootTest @OverrideAutoConfiguration(enabled = false) @SpringBootApplication public class BootTest extends AbstractTestNGSpringContextTests { @Test public void testName() throws Exception { } }
再查看输出的日志,就会发现Auto Configuration已经关闭。
@TestConfiguration是Spring Boot Test提供的一种工具,用它我们可以在一般的@Configuration之外补充测试专门用的Bean或者自定义的配置。
@TestConfiguration实际上是一种@TestComponent,@TestComponent是另一种@Component,在语义上用来指定某个Bean是专门用于测试的。
需要特别注意,你应该使用一切办法避免在生产代码中自动扫描到@TestComponent。 如果你使用@SpringBootApplication启动测试或者生产代码,@TestComponent会自动被排除掉,如果不是则需要像@SpringBootApplication一样添加TypeExcludeFilter:
//... @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), // ...}) public @interface SpringBootApplication
@TestConfiguration和@Configuration不同,它不会阻止@SpringBootTest去查找机制(在Chapter 1: 基本用法 – 使用Spring Boot Testing工具 – 例子4提到过),正如@TestConfiguration的javadoc所说,它只是对既有配置的一个补充。
所以我们在测试代码上添加@SpringBootConfiguration,用@SpringBootTest(classes=…)或者在同package里添加@SpringBootConfiguration类都是可以的。
而且@TestConfiguration作为内部类的时候它是会被@SpringBootTest扫描掉的,这点和@Configuration一样。
测试代码TestConfigurationTest:
@SpringBootTest @SpringBootConfiguration public class TestConfigurationTest extends AbstractTestNGSpringContextTests { @Autowired private Foo foo; @Test public void testPlusCount() throws Exception { assertEquals(foo.getName(), "from test config"); } @TestConfiguration public class TestConfig { @Bean public Foo foo() { return new Foo("from test config"); } } }
@TestConfiguration能够:
要特别注意第二点,@TestConfiguration能够直接覆盖已存在的Bean,这一点正常的@Configuration是做不到的。
我们先提供了一个正常的@Configuration(Config):
@Configuration public class Config { @Bean public Foo foo() { return new Foo("from config"); } }
又提供了一个@TestConfiguration,在里面覆盖了foo Bean,并且提供了foo2 Bean(TestConfig):
@TestConfiguration public class TestConfig { // 这里不需要@Primary之类的机制,直接就能够覆盖 @Bean public Foo foo() { return new Foo("from test config"); } @Bean public Foo foo2() { return new Foo("from test config2"); } }
测试代码TestConfigurationTest:
@SpringBootTest(classes = { Config.class, TestConfig.class }) public class TestConfigurationTest extends AbstractTestNGSpringContextTests { @Qualifier("foo") @Autowired private Foo foo; @Qualifier("foo2") @Autowired private Foo foo2; @Test public void testPlusCount() throws Exception { assertEquals(foo.getName(), "from test config"); assertEquals(foo2.getName(), "from test config2"); } }
再查看输出的日志,就会发现Auto Configuration已经关闭。
在上面的这个例子里的TestConfig是会被@ComponentScan扫描到的,如果要避免被扫描到,在本文开头已经提到过了。
先来看一下没有做任何过滤的情形,我们先提供了一个@SpringBootConfiguration(IncludeConfig):
@SpringBootConfiguration @ComponentScan public interface IncludeConfig { }
然后有个测试代码引用了它(TestConfigIncludedTest):
@SpringBootTest(classes = IncludeConfig.class) public class TestConfigIncludedTest extends AbstractTestNGSpringContextTests { @Autowired(required = false) private TestConfig testConfig; @Test public void testPlusCount() throws Exception { assertNotNull(testConfig); } }
从这段代码可以看到TestConfig被加载了。
现在我们使用TypeExcludeFilter来过滤@TestConfiguration(ExcludeConfig1):
@SpringBootConfiguration @ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class) }) public interface ExcludeConfig1 { }
再来看看结果(TestConfigExclude_1_Test):
@SpringBootTest(classes = ExcludeConfig1.class) public class TestConfigExclude_1_Test extends AbstractTestNGSpringContextTests { @Autowired(required = false) private TestConfig testConfig; @Test public void test() throws Exception { assertNull(testConfig); } }
还可以用@SpringBootApplication来排除TestConfig(ExcludeConfig2):
@SpringBootApplication public interface ExcludeConfig2 { }
看看结果(TestConfigExclude_2_Test):
@SpringBootTest(classes = ExcludeConfig2.class) public class TestConfigExclude_2_Test extends AbstractTestNGSpringContextTests { @Autowired(required = false) private TestConfig testConfig; @Test public void testPlusCount() throws Exception { assertNull(testConfig); } }