终极单元测试(1):基础工具介绍
期望
- 不依赖外部环境
- 结果校验
单元测试概要
- Mock接口(本地服务接口、RPC接口、http接口 )
- Mock数据库(zk/redis/mysql/mongodb)
- Mock第三方组件(dubbo/codis)
- 调用过程校验
- 执行结果校验
所有测试过程全部建立在与spring集成的基础上
工具
名称 | 作用 | 备注 |
---|---|---|
Junit | 单元测试框架 | Junit4 |
Spring Test | 提供Spring容器对测试进行支持 | Spring官方 |
Mockito | mock框架 | mock业务方法 |
PowerMock | mock框架 | 功能异常强大。完成终极单元测试的基石 |
DBunit | 自动化数据库处理 | 结合Spring Test DBUnit一起使用 |
H2 | 嵌入式数据库 | 可在内存中模拟mysql等主流数据库。JVM退出后消亡 |
curator-test | zk测试server | 在内存中启动zk server。JVM退出后消亡 |
embedded-redis | 嵌入式redis | 在内存中启动redis server。JVM正常退出后消亡 |
embedded-mongodb | 嵌入式mongodb | Embedded MongoDB |
embedded-xx | 嵌入式xx | xx是指其他任何第三方存储,比如kafka。若无则需要用PowerMock进行mock |
Junit
- 单元测试框架
Mockito
- 简介
测试案例,A调用B和CA -> B A -> C
mock案例,测试时A调用mock后的对象
A -> B_mocked A -> C_mocked
- 步骤
分三步走,第一步mock对象,第二步打桩(stub),第三步验证(verify) - 例子
@Test public void test(){ //mock对象 List<String> list = Mockito.mock(List.class); //打桩 when(list.get(Mockito.anyInt())).thenReturn("hello","world"); String result = list.get(0)+list.get(1); //验证 verify(list,Mockito.times(2)).get(Mockito.anyInt()); MatcherAssert.assertThat(result, Matchers.equalTo("helloworld")); }
- mock和spy
- mock的类的所有方法都会被mock
- spy的类的方法只有被打桩的方法才会进行mock,没被打桩的方法会调用实际方法
- spy的类使用when()会调用实际方法并返回mock的值
- spy的类使用doReturn()才会进行mock,不调用实际方法并返回mock的值
- 被@Spy注解修饰的变量必须要有初始值
- 缺点
因为mockito是使用bytebuddy生成子类代理来mock原始对象,所以不可以实现对静态函数、私有函数、本地方法的模拟.
PowerMock
- 简介
PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法 - 例子
@RunWith(PowerMockRunner.class) @PrepareForTest(TestStatic.class) public class Test { @Test public void test() throws Exception { PowerMockito.mockStatic(TestStatic.class); PowerMockito.when(TestStatic.staticMethod()).thenReturn("value"); } }
- 原理
- 当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。
- PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。
- 如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。
- 注意事项
- jdk7需要增加jvm参数:-noverify
- 遇到ClassNotFound错误,需要把对应的包名放到注解@PowerMockIgnore内(详见最后的项目代码示例)
- jdk7如果遇到OutOfMemoryError,则可以增加jvm参数-XX:PermSize=128m,或者你可以调整这个参数到更大
DBunit
- 简介
通过预处理数据来初始化数据库数据,通过预期数据来校验实际测试结果,具体流程如下建立数据库连接-> 备份表 -> 插入初始化数据 -> 执行测试-> 从数据库取实际结果-> 事先准备的期望结果 -> 断言 -> 回滚数据库 -> 关闭数据库连接
- 例子
单独的dbunit使用起来过于麻烦,详情例子可以查看dbunit官网 - 与spring结合
和spring结合,需要用到另外一个插件:Spring Test DBUnit,它提供了多个注解来方便测试,其中主要的三个注解是@DbUnitConfiguration(databaseConnection = {"dataSource"}) 指定数据源 @DatabaseSetup(value="data.xml", connection=dataSource, type=DatabaseOperation.CLEAN_INSERT) 插入初始化数据 @ExpectedDatabase(value="result.xml",connection=dataSource, assertionMode= DatabaseAssertionMode.NON_STRICT_UNORDERED) 校验期望的数据
H2
- 简介
不依赖于其他外部数据库,在本机内存中模拟数据库,避免因为脏数据产生的测试问题 - 劣势
不支持mysql的函数,只支持sql标准函数,具体可以去h2官网查看
Spring Test
- 原理
主要由三个接口构成:TestContextManager、TestContext、TestExcutionListener- TestContextManager 每次测试用例启动都会被创建,并且管理一个TestContext。执行与管理TestExcutionListener集合
- TestContext 负责持有一个当前测试的上下文,包含测试类的基本信息,加载的spring上下文
- TestExcutionListener 在单元测试开始或者结束的时候执行,可以自定义自己的监听
集成
Mockito+PowerMock+Junit4+Spring集成
- 不能mock被spring代理的对象,比如加了切面的类(例如事务切面)
- 被代理的类需要用AopTestUtils.getTargetObject()获得原始类
- 多层mock测试的只能@Mock最深层的类,中间类需要用@Spy
- @Spy修饰的属性必须要有初始值
Dbunit+H2+Junit4+Spring集成
- 使用h2在spring运行时替换对应的dataSource
- 注意h2的语法
终极集成
- 自定义MockH2TestExecutionListener并继承TestExcutionListener
- 集成com.github.springtestdbunit.DbUnitTestExecutionListener
- 并且按照以下顺序引入TestExcutionListener
@TestExecutionListeners({MockH2TestExecutionListener.class,DbUnitTestExecutionListener.class})