JMockit_Guideline
UT自动生成插件使用说明
- 特性
- IDEA插件使用步骤
- Maven插件使用步骤
- 代码结构说明
- 插件可选配置项
- Mock注意事项
- Mock点的选择
- 接口和抽象类的Mock
- 类初始化器和构造器的Mock
- 获取任意类型的Mock对象
- 私有字段的读写
- Release Notes
特性
- 自动生成被测类所有声明方法的测试代码
- 自动生成被测类依赖的Mock代码,并与测试代码分离
- 支持增量(新增类和方法)生成
- 不会覆盖已有测试代码
- 简单易用,生成速度快
- 配置灵活多变
- 准备环境
IDEA、Maven环境、Maven项目
- 安装
下载插件zip安装包,UTGenerator.zip
- 在IntelliJ IDEA中选择File > Settings > Pluggins,然后选择Install plugin from disk...如下图:
选择下载的UTGenerator.zip,然后OK,IDEA会提示重启,重启完成后插件就安装好了。
注意:不要解压zip包,直接选择UTGenerator.zip进行安装,如果只安装zip包中lib下的UTGenerator.jar可能会出现java.lang.NoClassDefFoundError: javassist/NotFoundException。
5 . 执行插件
在Project或Module的任意目录或文件上右击,选中第二项的Generate UT,然后会弹出如下对话框:
6. 配置项说明
本地Maven仓库:一般不用填写,默认自动通过mvn help:effective-settings命令获取仓库地址,若实际仓库地址与此命令读取的地址不同,则填写实际仓库地址
文件输出路径:可不填,默认为项目的src\test\java目录
不生成默认fail:默认生成的测试代码最后都有一句fail("This test case is a prototype."),勾选则不生成
不生成Mock代码:勾选后不生成任何Mock相关代码
交互式Mock:默认生成的Mock代码为全量Mock,在交互式Mock模式下提供可视化的依赖树界面让用户选择需要进行Mock的依赖,如下图
每一个存在依赖的被测类都会弹出一个对话框,用于展示被测类的所有依赖,例如上图中FlightweightFactory为被测类,红色斜体的是它的方法,
蓝色的类为当前方法依赖的类,而黑色的方法为具体依赖的类中的哪些方法。对依赖项进行勾选,然后点击OK,最终生成的Mock代码中就只包含勾选的那些依赖。
注意:插件运行时会对所选的project和module进行编译,所以插件运行前请确保编译能够成功(单独module编译失败时需要对整个project进行mvn install -DskipTests);在交互式Mock模式下选择的被测类个数上限为50个;添加Jmockit依赖可以参考下面的Maven插件使用步骤3。
Maven插件使用步骤
- 准备环境
插件运行需jdk1.8,生成的代码要求jdk1.7+
2 在pom.xml中:
注意:1. plugin配置要加在build节点下的plugins节点下,而非只添加在pluginManagement节点下面;在包含多个module的maven项目中推荐把UT插件依赖添加在外层父pom中,然后在需要生成UT的module根目录下执行mvn ut:generate,idea中则可以在子模块的Plugins节点下双击ut:generate。
2. 插件运行时会首先编译项目,所以插件成功运行的前提是项目能够编译通过,即能够在待生成UT的project或mudule下执行mvn clean compile成功,如果在子module下执行编译失败,可以先把整个project进行mvn clean install -DskipTests,因为子module单独编译失败往往是没有把其依赖的其他模块的jar包安装到本地maven仓库导致的。
3. 当出现java.lang.ClassNotFoundException时,在保证编译(mvn clean compile)成功的前提下,检查一下UT插件读取的maven仓库地址(插件运行时会显示)与mvn install的仓库地址是否一致,因为UT插件会从本地maven仓库中加载依赖,而在命令行(系统的cmd或者idea中的terminal)中执行mvn命令与idea图形界面中双击执行时读取到的maven本地仓库地址可能是不一致的,这个不一致是由于idea的maven选项中User settings file或者Local repository勾选了override,所以可以尝试去除勾选override再重新运行UT插件。
4. 当出现类似Error injecting: com.ctrip.flight.testframework.tools.GenerateMojojava.lang.TypeNotPresentException: Type com.ctrip.flight.testframework.tools.GenerateMojo not present的错误时,需要把项目的jdk切换成1.8,执行完插件后可以切回1.7。
代码结构说明
自动生成的UT代码保存在项目目录中的src\test\java\下,每个被测类会生成两个文件,例如被测类App.java会对应生成AppAutoTest.java和AppAutoMock.java,前者是测试代码,后者是Mock代码,三者代码内容关系用下面一个简单的例子说明:
(1)被测类App.java 内容
public class App { private OuterService outerServiceA = new OuterService( "serviceA" ); private OuterService outerServiceB = new OuterService( "serviceB" ); //方法依赖OuterService#isNormal方法,并调用了两次 public boolean checkStatus() { boolean b1 = outerServiceA.isNormal(); boolean b2 = outerServiceB.isNormal(); return b1 && b2; } } |
(2)AppAutoMock.java内容
public class AppAutoMock { //每个被测方法都会对应一个静态内部类,类名为被测方法名+Mock static class CheckStatusMock { //每个被测方法依赖的类都会对应一个静态方法,该方法命名方式为mock+依赖的类名。静态方法会把被测方法依赖此类的所有方法Mock掉。接收的Map参数为Mock方法被调次数(从0开始计数)与返回值之间的映射。 static void mockOuterService( final Map isNormalMockValue) { new MockUp() { //记录isNormal方法被调次数 int isNormalCount = 0 ; //保存最后一次调用的返回值,当实际调用次数多于Map中给出的返回值个数时返回此值 boolean lastIsNormalMockValue; //Mock被测方法App#checkStatus依赖的isNormal方法 @Mock public boolean isNormal() { boolean result = lastIsNormalMockValue; if (isNormalMockValue.containsKey(isNormalCount)) { result = isNormalMockValue.get(isNormalCount); lastIsNormalMockValue= result; } isNormalCount++; return result; } }; } } } |
(3)AppAutoTest.java 内容:
@RunWith (JMockit. class ) //继承AppAutoMock public class AppAutoTest extends AppAutoMock { private App testInstance; @Before public void setUp() { //可以复用 testInstance = new App(); } //方法名为test+被测方法名,如果是重载方法则为test+被测方法名+下划线+参数类型组合 @Test public void testCheckStatus() { //mock class OuterService boolean z = true ; Map isNormalMockValue = new HashMap<>(); //插件自动给出第一次调用Mock方法时返回的值,使用者可以按需修改,也可以添加Mock方法第二次及更多次调用时返回的值(存在这样的场景),如isNormalMockValue.put(1,false),即给定第二次调用OuterService.isNormal方法时返回false isNormalMockValue.put( 0 , z); CheckStatusMock.mockOuterService(isNormalMockValue); //调用被测方法 boolean actualResult = testInstance.checkStatus(); //断言 assertFalse(actualResult); //TODO: remove this default call to fail fail( "This test case is a prototype." ); } } |
说明:1. Mock框架使用JMockit(快速上手),相比于Mockito、EasyMock等Mock工具其功能更加强大,能够方便地让你对单元测试中的final类、静态方法、构造函数、private static final属性进行mock。
2. 不同的测试方法(@Test标注的方法)的Mock操作是相互隔离的,不会相互影响,即当前测试方法中调用的Mock操作产生的Mock效果在方法执行结束后也就失效了。
3. @Mock标注的被mock方法,默认逻辑为根据被调用次数返回相应的Mock值,使用者可以对其进行重写,实现任意自定义逻辑,比如验证Mock方法被调用的次数、抛出异常以模拟异常情况, 还可以调用原有方法实现(借助JMockit中的Invocation)。
4. 每个生成的Test类上面都标注有@RunWith(JMockit.class),可以删除,这样就可以替换成其他的Runner,比如替换成@RunWith(SpringJUnit4ClassRunner.class),使测试在Spring容器环境下执行,但此时必须保证项目pom文件中JMockit依赖写在Junit依赖之前(JMockit官方文档中有说明)。
5. 被测类中新增方法后,再次运行UT插件,只会生成新增方法的测试代码和Mock代码,追加到原文件内容的后面。
插件可选配置项
< plugin > < groupId >com.ctrip.flight.testframework.tools groupId > < artifactId >ut-maven-plugin artifactId > < version >2.2.3 version > < executions > < execution > < phase >none phase > < goals > < goal >generate goal >
goals >
execution >
executions > < configuration >
< onlyIncludeGeneratePackages > < param >xxx param >
onlyIncludeGeneratePackages >
< onlyExcludeGeneratePackages > < param >xxx param >
onlyExcludeGeneratePackages >
< outputPath >D:\xxx outputPath >
< closeMock >false closeMock >
< onlyIncludeMockPackageMethods > < param >packageA param > < param >packageB:someMethod param >
onlyIncludeMockPackageMethods >
< excludeMockPackages > < param >xxx.xxx param >
excludeMockPackages >
< excludeMockMethodPrefixes > < prefix >somePrefix prefix >
excludeMockMethodPrefixes >
< excludeMockMethodSuffixes > < suffix >someSuffix suffix >
excludeMockMethodSuffixes >
< excludeMockPackageMethodPrefixes > < packageMethodPrefix >somePackage:methodPrefix packageMethodPrefix >
excludeMockPackageMethodPrefixes >
< excludeMockPackageMethodSuffixes > < packageMethodSuffix >somePackage:methodSuffix packageMethodSuffix >
excludeMockPackageMethodSuffixes >
< fileSuffix >blank fileSuffix >
< preferConstructorWithParams >false preferConstructorWithParams >
< closeDefaultCallToFail >true closeDefaultCallToFail >
< addClassPaths > < path >D:\lib\xxx.jar path >
addClassPaths >
configuration >
plugin > |
说明:
- 当不配置onlyIncludeGeneratePackages和onlyExcludeGeneratePackages时,插件会生成当前项目所有类的测试代码,即默认全量生成。
- closeMock配置项是全局的Mock开关,设为true时所有Mock相关的代码都不会生成,包括Mock类和Test类中的Mock操作代码。
- 添加onlyIncludeMockPackageMethods配置 ,实现只对特定的依赖类和方法生成Mock代码,此时excludeMock前缀的配置项会被忽略。
- excludeMockMethodPrefixes和excludeMockMethodSuffixes配置项会对所有包和类中的方法进行匹配,而excludeMockPackageMethodPrefixes和excludeMockPackageMethodSuffixes只对“:”(英文冒号)之前所表示的包和类中的方法进行匹配,即前者是全量匹配后者是部分匹配。
- 可以在命令行中传入需要生成单元测试的类名或包名,例如mvn ut:generate -Dtarget=xxx 其中xxx为需要生成UT的包名、全类名、类名或者它们的前缀,多个名称用英文逗号隔开,这样就只会对符合条件的类生成UT。
- 可以在命令行中传入目标输出文件的路径,例如mvn ut:generate -DoutputPath=D\:test,代表生成的文件会放在D盘test文件夹中。可以结合target选项一起使用,例如mvn ut:generate -DoutputPath=D\:test -Dtarget=xxx。
- 当项目存在非maven管理的依赖时,需要加上addClassPaths配置项,否则运行UT插件会出现ClassNotFoundException。
Mock注意事项
Mock点的选择
在单元测试时,被测试代码可能会依赖其他代码,可以通过第三方Mock框架来mock被依赖的代码,从而进行隔离测试。
在使用Mock的过程中,发现如何选择Mock点非常关键,会直接影响到测试效果。Mock并不是越多越好,过多的Mock往往会得不偿失,同时不恰当的Mock选择反而会对我们的测试产生误导,从而在后期的集成和系统测试中引入更多的问题。
在Mock点的选择过程中,以下的一些点会是一些不错的选择:
- 网络交互:如果两个被测模块之间是通过网络进行交互的,那么对于网络交互进行mock通常是比较合适的,如RPC
- 外部资源:比如文件系统、数据源,如果被测对象对此类外部资源依赖性非常强,而其行为的不可预测性很可能导致测试的随机失败,此类的外部资源也适合进行mock
- 真实对象的某些行为很难触发,比如某些异常的逻辑在正常测试中很难触发,通过Mock可以人为的控制触发异常逻辑
- 真实情况令程序的运行速度很慢,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行mock
- 真实对象实际上并不存在(当需要和其他开发小组进行合作,对方接口功能还没有开发完成)
UT插件在生成Mock代码时除了会对jdk和一些公共类库如guava进行排除之外,其他被测方法依赖的类和方法都会进行mock,包括同一类中的其他方法,即mock了所有的外部依赖(最纯粹的UT)。这样自动生成的代码中就会包含一些不必要的Mock操作,比如pojo的get、set方法的mock,使用者需要根据实际情况进行筛选。在保留有意义的Mock代码时除了注释或删除其它Mock代码这种方式之外,还可以按照插件可选配置项中介绍的添加排除mock的选项,就能够不生成相应的Mock代码。例如在excludeMockPackages配置项中加上被测项目所在的包名,那么被测方法的所有依赖(类和方法)中属于当前被测项目的部分都不会进行mock,只会对不属于被测项目中的其他依赖(类和方法)生成Mock代码。
如果需要mock的依赖很少,而且事先已经知道哪些依赖类和方法需要进行mock,通过上述配置排除mock的方式会比较麻烦,此时可以配置onlyIncludeMockClassMethods选项 ,只对需要mock的生成Mock代码,避免了全量mock生成过多的代码,这种方式是比较推荐的。
默认不进行mock的包名前缀如下:
- java.
- sun.
- javax.
- org.xml
- org.w3c
- org.omg.
- sunw.
- org.jcp.
- org.ietf.
- daikon.
- com.google.common
- org.exsyst
- org.joda.time
-
接口和抽象类的Mock
上一节例子中的Mock代码较简单,没有涉及到接口或抽象类的Mock,当被测方法中存在接口调用时,例如存在接口:
interface IDpend{
void foo();
}
被测方法内部这样使用IDepend:
IDepend depend1 = new DependA();
depend1.foo();
其中DependA是IDepend的一个实现类,这时自动生成的Mock代码会对对IDepend接口进行mock:
public static extends IDepend> void doMock() {
new MockUp() { @Mock void foo() { } }; } |
以上Mock代码会导致实现IDepend接口的所有类(包括匿名类)的foo方法都会被mock掉,而之所以没有生成对DependA的MockUp,是因为UT插件只识别对象的编译时类型,而非运行时类型,所以如果被测方法中同时存在这样的代码:
DependA depend2 = new DependA();
depend2.foo();
那此时自动生成的mock代码中就会同时出现对DependA的MockUp,即:
new MockUp() { @Mock void foo() { } }; |
而此时depend1和depend2对象的foo方法会被哪个MockUp给mock掉呢,答案是MockUp,即优先寻找运行时类型所对应的MockUp,如果没有则寻找其接口的MockUp。
如果此时被测方法里还有IDepend接口的另外一个实现类DependB的方法调用:
IDepend depend3 = new DependB();
depend3.foo();
显然此时depend3的foo方法会被IDpend接口的MockUp给mock掉(因为没有对DependB的MockUp)。
如果此时不想对DependB进行mock,但我们又不能把对IDepend接口的MockUp给删掉,因为它可能还有用处,比如用于对DependC、DependD、DependE...进行mock,如何做到对其中的一个实现类DependB不进行mock呢?此时前面提到的JMockit中的Invocation就派上用场了,借助强大的Invocation,只需对IDpend接口的MockUp稍作修改即可:
public static extends IDepend> void doMock() {
new MockUp() { @Mock void foo(Invocation invo) { if (invo.getInvokedInstance() instanceof DependB) { invo.proceed(); } } }; } |
实现原理很清晰,调用DependB的foo方法还是进入了IDepend的MockUp中的foo方法,只是这里增加了类型判断,判断被调用foo方法的实例的运行时类型,如果是DependB就执行invo.proceed(),即调用原方法实现,达到了不进行mock的效果。
类初始化器和构造器的Mock
如果被测类的类初始化器(static代码块,static字段初始化)和构造函数存在一些依赖性的操作,也可以对其进行mock。自动生成的Mock文件中包含相关的Mock代码,在使用相关的Mock方法时需要注意一下代码注释中的说明,即需要把握好类初始化器和构造器的Mock方法的调用时机,否则会不起作用。
获取任意类型的Mock对象
可以通过生成的Mock文件中的getMockInstance方法来获取任意类型的Mock对象,尤其是对于接口和抽象类型十分便利。
私有字段的读写
每个自动生成的Mock文件中都默认包含setField、setStaticField、getField、getStaticField方法,能够方便地读写对象的私有字段和类的私有静态字段。
在实例化被测类时,由于可能依赖于特定环境导致构造函数执行或者字段初始化失败,从而可能在执行测试方法时出现NullPointerException,进而导致相关的Mock操作也无法生效,此时可以借助前面三种手段(构造器的Mock,任意类型的Mock对象,私有字段的set)来初始化被测对象,具体方式为:
- 在实例化被测类之前调用Mock文件中提供的构造器的Mock方法
- 实例化被测类得到被测实例,由于mock掉了构造器,被测实例的字段为未初始化状态
- 通过Mock文件中的getMockInstance方法获取需要初始化的字段类型的Mock对象
- 通过setField方法把第三步中得到的Mock对象设置到被测实例中
关于JMockit的更多细节请参考官方文档:Jmockit官方教程