Spring、Spring Boot 和 TestNG 测试指南 ( 3 )

Spring&Spring Boot Testing工具提供了一些方便测试的Annotation,本文会对其中的一些做一些讲解。

@TestPropertySource

@TestPropertySource可以用来覆盖掉来自于系统环境变量,Java的系统属性,@PropertySource的属性。

同时@TestPropertySource(properties=…)优先级高于@TestPropertySource(locations=…)。

利用它我们可以很方便的在测试代码里微调,模拟配置(比如修改操作系统目录分隔符,数据源等)。

例子1:使用Spring Testing工具

我们先使用@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())
    );
  }
}

例子2:使用Spring Boot Testing工具

@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

@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 ”);
  }

}

例子1:不使用ActiveProfiles

在没有@ActiveProfiles的时候,外形=默认和没有设定个人资料的豆会被加载到。

源代码ActiveProfileTest:

@ContextConfiguration(类 =  配置。类)
 公共 类 ActiveProfileTest  延伸 AbstractTestNGSpringContextTests {

  @Autowired 
  私人 Foo foo;

  @Autowired 
  私人 酒吧 ;

  @Test 
  public  void  test(){
    的assertEquals(FOO 。的getName(), “默认”);
    的assertEquals(巴。的getName(), “无简档”);
  }

}

例子二:使用ActiveProfiles

当使用了@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配合使用,这里就不举例说明了。

Annotations - @JsonTest

@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对于日期时间类型处理的例子

例子1:简单例子

源代码见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);

  }

}

例子2: 测试@JsonComponent

@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);

  }

}

例子3: 使用@ContextConfiguration

事实上@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 {
    // ...
  }

}

@OverrideAutoConfiguration

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

例子1:未关闭Auto Configuration

源代码见BootTest:

@SpringBootTest
@SpringBootApplication
public class BootTest extends AbstractTestNGSpringContextTests {

  @Test
  public void testName() throws Exception {

  }
}

查看输出的日志,会发现Auto Configuration已经启用。

例子2:关闭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

@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

例子1:作为内部类

@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");
    }

  }
}

例子2:对@Configuration的补充和覆盖

@TestConfiguration能够:

  1. 补充额外的Bean
  2. 覆盖已存在的Bean

要特别注意第二点,@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已经关闭。

例子3:避免@TestConfiguration被扫描到

在上面的这个例子里的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);

  }

}

参考文档



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部