一、背景
在公司的CleanCode的大背景下,践行开发者自测试(TDD)也是CleanCode文化比较重要的内容之一。
在目前主流的微服务的架构中,SpringBoot+Spring Cloud的框架是非常主流的微服务的架构,并且PowerMock也是非常强大的Mock的工具,PowerMock可以基于EasyMock,也可以基于Mockito,PowerMock是两者的增强版,因此是非常的强大。如果在SpringBoot的框架上整合PowerMock,那么应该是非常完美的。
基于此背景下,笔者因此动手亲自的在SpringBoot的微服务框架上搭建PowerMock的功能,在搭建的过程中遇到了非常非常多的坑,记录下来,算是经验的总结和教训!
二、环境准备
2.1、 PowerMock依赖的引入
笔者的SpringBoot是2.0+的版本,因此各位在引入的时候需要注意一下,PowerMock的版本和SpringBoot版本的兼容性,以防一开始就开始采坑!
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.28.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>2.0.2-beta</version>
<scope>test</scope>
</dependency>
</dependencies>
2.2、搭建基础的目录结构
对于Maven的目录结构,相信很多人都不陌生。
但是这里我还是对下面的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
@SpringBootApplication
@ComponentScan(basePackages = {"com.huanghe.springcloud"}, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {MyConfig.class})
})
public class TestApplication {
public TestApplication() {
System.out.println("启动类TestApplication执行了");
}
public static void main(String[] args) {
System.out.println("TestApplication 执行了");
SpringApplication.run(TestApplication.class, args);
}
}
- 这个类为什么需要使用SpringBootApplication
- @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
/**
* 功能描述
* SpringBootTest 作用:1.标记当前类为测试类 2.加载spring-boot启动类,启动spring-boot
* @author h00518386
* @since 2021-11-16
*/
@SpringBootTest(classes = TestApplication.class)
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({
"javax.management.*", "javax.net.ssl.*", "javax.crypto.*", "javax.xml.*", "org.xml.*", "org.w3c.dom.*",
"org.apache.*"
})
public class DemoApplicationTests {
@Value("${my}")
private String str;
@Autowired
TestService testService;
@Test
public void testDemo() {
System.out.println("demo applicaiton is " + str);
}
}
- 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的作用有两个:
- 标记当前类为测试类
- 加载spring-boot启动类,启动spring-boot
@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
上面的注解生效了
@SpringBootApplication
@ComponentScan(basePackages = {"com.huanghe.springcloud"}, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {MyConfig.class, StarterAppliaction.class})
})
public class TestApplication {
......
}
三、打印日志的坑
在一切准备好了,之后启动的时候,会发现控制台打印了非常多的Debug
的日志,在logback-test.xml
文件中,修改各种都不生效,结果就是一个大坑…. , 以此记录一下
<!-- 指定控制台日志输出格式 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level [%X{taskId}] %logger{35} [%file:%line] %msg%n
</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
参考文章
@SpringBootTest注解分析(一)Found multiple @SpringBootConfiguration annotated classes
解决SpringBoot @CompentScan excludeFilters配置无效的方案