一、背景

  1. 在公司的CleanCode的大背景下,践行开发者自测试(TDD)也是CleanCode文化比较重要的内容之一。
  2. 在目前主流的微服务的架构中,SpringBoot+Spring Cloud的框架是非常主流的微服务的架构,并且PowerMock也是非常强大的Mock的工具,PowerMock可以基于EasyMock,也可以基于MockitoPowerMock是两者的增强版,因此是非常的强大。如果在SpringBoot的框架上整合PowerMock,那么应该是非常完美的。
  1. 基于此背景下,笔者因此动手亲自的在SpringBoot的微服务框架上搭建PowerMock的功能,在搭建的过程中遇到了非常非常多的坑,记录下来,算是经验的总结和教训!

二、环境准备

2.1、 PowerMock依赖的引入

笔者的SpringBoot是2.0+的版本,因此各位在引入的时候需要注意一下,PowerMock的版本和SpringBoot版本的兼容性,以防一开始就开始采坑!

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>
  6. <dependency>
  7. <groupId>junit</groupId>
  8. <artifactId>junit</artifactId>
  9. <scope>test</scope>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.powermock</groupId>
  13. <artifactId>powermock-module-junit4</artifactId>
  14. <version>2.0.2</version>
  15. <scope>test</scope>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.powermock</groupId>
  19. <artifactId>powermock-api-mockito2</artifactId>
  20. <version>2.0.2</version>
  21. <scope>test</scope>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.mockito</groupId>
  25. <artifactId>mockito-core</artifactId>
  26. <version>2.28.2</version>
  27. <scope>test</scope>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.assertj</groupId>
  31. <artifactId>assertj-core</artifactId>
  32. <version>3.11.1</version>
  33. <scope>test</scope>
  34. </dependency>
  35. <dependency>
  36. <groupId>org.mockito</groupId>
  37. <artifactId>mockito-all</artifactId>
  38. <version>2.0.2-beta</version>
  39. <scope>test</scope>
  40. </dependency>
  41. </dependencies>

2.2、搭建基础的目录结构

对于Maven的目录结构,相信很多人都不陌生。

image.png

但是这里我还是对下面的test目录下的目录结构进行简单的介绍,以及我这么分的原因。首先介绍资源文件,在test\resouces资源文件中和main\resource资源文件中的一样,我这里也是存在基础的配置文件spring.profiles.active = junit的配置文件application.yml

test\java目录下的是两个java文件,这两个java文件是非常重要的,第一个java测试启动配置文件 TestApplication.java。第二个是测试类文件DemoApplication.java。很多人可能会比较疑惑为什么需要在整一个测试启动类配置文件,在说之前我们先看看测试启动配置文件:

2.3、启动类TestApplication

TestApplication.java

  1. @SpringBootApplication
  2. @ComponentScan(basePackages = {"com.huanghe.springcloud"}, excludeFilters = {
  3. @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {MyConfig.class})
  4. })
  5. public class TestApplication {
  6. public TestApplication() {
  7. System.out.println("启动类TestApplication执行了");
  8. }
  9. public static void main(String[] args) {
  10. System.out.println("TestApplication 执行了");
  11. SpringApplication.run(TestApplication.class, args);
  12. }
  13. }
  1. 这个类为什么需要使用SpringBootApplication
  2. @ComponentScan的作用?为什么@Component注解需要排除StarterApplication.class

在能回答这两个问题之前是遇到了非常多的坑,走了极其多的弯路,做过了很多次的验证,之后得到的结果。。。。。。

1 :为什么我们还需要一个启动类,我们在main中目录下已经定义了启动类,为什么需要多次一举呢,因为我们在main中定义的启动类,一般就是我们运行的启动类(在MANIFEST.MF文件中指定的启动类),在main目录下定义的启动类,上面一般加了很多的注解,例如@EnableFeignClients() @EnableDiscoveryClient等等,这些其实在我们的 DT单元测试中是用不到的。因此我们的单元测试启动类应该是最小化的配置,并且可以自定义,一般就定义了一个测试的启动类TestApplicaiton

2:在启动的时候我们一般会指定扫描路径@ComponentScan。如果不指定的话,就会在启动类所在的包路径下进行扫描,也就是SpringBoot的约定大于配置(OS:想用好SpringBoot,那么你得知道这些约定,要不然就是一堆潜规则~~~)。话题转回来,刚刚讲了我们指定了扫描注解@ComponentScan的路径之后,当我们不需要某个配置类生效此时,比较常见的做法就是excludeFilters进行过滤掉,这里的过滤方式有注解、自定义、类、正则。在上面的TestApplication.java的代码中,过滤的例子就是MyConfig.class。这里做的目的是:当项目中有很多的配置需要加载的时候,我们其实可以过滤掉很多的不需要配置,例如kafka、Eureka...,使得单元测试不需要依赖环境,而成为了集成测试

2.4、测试类DemoApplicaitonTest

DemoApplicationTests.java

  1. /**
  2. * 功能描述
  3. * SpringBootTest 作用:1.标记当前类为测试类 2.加载spring-boot启动类,启动spring-boot
  4. * @author h00518386
  5. * @since 2021-11-16
  6. */
  7. @SpringBootTest(classes = TestApplication.class)
  8. @RunWith(PowerMockRunner.class)
  9. @PowerMockRunnerDelegate(SpringRunner.class)
  10. @PowerMockIgnore({
  11. "javax.management.*", "javax.net.ssl.*", "javax.crypto.*", "javax.xml.*", "org.xml.*", "org.w3c.dom.*",
  12. "org.apache.*"
  13. })
  14. public class DemoApplicationTests {
  15. @Value("${my}")
  16. private String str;
  17. @Autowired
  18. TestService testService;
  19. @Test
  20. public void testDemo() {
  21. System.out.println("demo applicaiton is " + str);
  22. }
  23. }
  • SpringBootTest:表明这是一个springboot测试类,class指定的是springboot主启动程序类
  • RunWith:使用powermock自己的Runner,每一个 Runner 都有各自的特殊功能,你要根据需要选择不同的 Runner 来运行你的测试代码。JUnit 中有一个默认 Runner ,即 BlockJUnit4ClassRunner
  • PowerMockRunnerDelegate:将powermock整合到spring容器中。集成PowerMock的时候因为Junit的Runner只能设置一个,所以不知道该设置
    • PowerMockRunner还是SpringRunner。如果设置了PowerMockRunner,虽然可以使用mock和spy功能,但是无法使用Spring提供的功能。
    • PowerMockRunnerDelegate解决了此问题,既可以使用PowerMock的强大的mock和Spy的功能,也可以使用Spring提供的功能
  • PowerMockIgnore: 避免两个类的ClassLoader不同,导致JVM认为这两个类没有关联。PowerMock的工作原理即是使用自定义的类加载器来加载被修改过的类,从而达到打桩的目的。(这里需要是因为不加的话,加解密的时候是会报错的)
  • TestPropertySource: 解决SpringBoot默认加载的顺序第一优先级会加载file:./config/目录下的文件,导致test目录下的application.yml无法被加载的问题,指定加载的yaml文件

这里重点讲一下SpringBootTest,SpringBootTest的作用有两个:

  1. 标记当前类为测试类
  2. 加载spring-boot启动类,启动spring-boot
  3. @SpringBootTest可以指定启动类@SpringBootTest(classes = TestApplication.class),表示的是启动的是TestApplicaiton.java作为启动类。如果不指定就会默认的寻找注解了@SpringBootApplication的类进行加载

补充:对于第3点其实里面是存在着很大的坑的。我在调试excludeFilters,过滤不需要的配置的时候,在TestApplication@ComponentScan中加入的过滤,其实是一直都不会生效的,想过很多的办法,找过很多的资料,最终跟随这@SpringBootTest源码才发现,尽管我们在@SpringBootTest中指定了启动类TestApplicaiton.java。但是在启动的时候是仍然会加载StarterAppliaction.java的。。。。。这里就会出现在TestApplication@ComponentScan中加入的过滤,和StarterAppliaction.java@ComponentScan出现相互干扰。通过各种验证手段,你最终会发现TestApplication.java是会先加载的。那么这样的话,我只想保留一个简约的启动类,另外一个启动类屏蔽掉就Ok,然后就在TestApplication@ComponentScan中加入的过滤StarterAppliaction.java。最后TestApplication@ComponentScan的终极版本应该是:

这样才使得只有有个启动类生效,@ComponentScan也就只有TestApplicaiton上面的注解生效了

  1. @SpringBootApplication
  2. @ComponentScan(basePackages = {"com.huanghe.springcloud"}, excludeFilters = {
  3. @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {MyConfig.class, StarterAppliaction.class})
  4. })
  5. public class TestApplication {
  6. ......
  7. }

三、打印日志的坑

在一切准备好了,之后启动的时候,会发现控制台打印了非常多的Debug的日志,在logback-test.xml文件中,修改各种都不生效,结果就是一个大坑…. , 以此记录一下

  1. <!-- 指定控制台日志输出格式 -->
  2. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  3. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  4. <Pattern>
  5. %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level [%X{taskId}] %logger{35} [%file:%line] %msg%n
  6. </Pattern>
  7. <charset>UTF-8</charset>
  8. </encoder>
  9. </appender>

参考文章

@SpringBootTest注解分析(一)Found multiple @SpringBootConfiguration annotated classes

解决SpringBoot @CompentScan excludeFilters配置无效的方案

Spring @Configuration 和 @Component 区别

logback 的配置