单元模拟测试--UT 自动生成+JMockit 工具

JMockit_Guideline

 

UT自动生成插件使用说明

 

  • 特性
  • IDEA插件使用步骤
  • Maven插件使用步骤
  • 代码结构说明
  • 插件可选配置项
  • Mock注意事项
    • Mock点的选择
    • 接口和抽象类的Mock
    • 类初始化器和构造器的Mock
    • 获取任意类型的Mock对象
    • 私有字段的读写
  • Release Notes

 

特性

  1. 自动生成被测类所有声明方法的测试代码
  2. 自动生成被测类依赖的Mock代码,并与测试代码分离
  3. 支持增量(新增类和方法)生成
  4. 不会覆盖已有测试代码
  5. 简单易用,生成速度快
  6. 配置灵活多变

 

 

 

  1. 准备环境
    IDEA、Maven环境、Maven项目
     
  2. 安装
    下载插件zip安装包,UTGenerator.zip
  3. 在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插件使用步骤

  1. 准备环境
    插件运行需jdk1.8,生成的代码要求jdk1.7+

 

     2 在pom.xml中:

单元模拟测试--UT 自动生成+JMockit 工具_第1张图片

 

 

注意: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.toolsgroupId>

    <artifactId>ut-maven-pluginartifactId>

    <version>2.2.3version>

    <executions>

        <execution>

            <phase>nonephase>  

            <goals>

                <goal>generategoal>

            goals>

        execution>

    executions>

    <configuration>

        

        <onlyIncludeGeneratePackages>

            <param>xxxparam>

        onlyIncludeGeneratePackages>

        

        <onlyExcludeGeneratePackages>

            <param>xxxparam>

        onlyExcludeGeneratePackages>

        

        <outputPath>D:\xxxoutputPath>

        

        <closeMock>falsecloseMock>

        

        <onlyIncludeMockPackageMethods>

            <param>packageAparam>

            <param>packageB:someMethodparam>

        onlyIncludeMockPackageMethods>

        

        <excludeMockPackages>

            <param>xxx.xxxparam>

        excludeMockPackages>

        

        <excludeMockMethodPrefixes>

            <prefix>somePrefixprefix>

        excludeMockMethodPrefixes>

        

        <excludeMockMethodSuffixes>

            <suffix>someSuffixsuffix>

        excludeMockMethodSuffixes>

        

        <excludeMockPackageMethodPrefixes>

            <packageMethodPrefix>somePackage:methodPrefixpackageMethodPrefix>

        excludeMockPackageMethodPrefixes>

        

        <excludeMockPackageMethodSuffixes>

            <packageMethodSuffix>somePackage:methodSuffixpackageMethodSuffix>

        excludeMockPackageMethodSuffixes>

        

        <fileSuffix>blankfileSuffix>

        

        <preferConstructorWithParams>falsepreferConstructorWithParams>

        

        <closeDefaultCallToFail>truecloseDefaultCallToFail>

        

        <addClassPaths>

            <path>D:\lib\xxx.jarpath>

        addClassPaths>

    configuration>

plugin>

说明:

  1. 当不配置onlyIncludeGeneratePackages和onlyExcludeGeneratePackages时,插件会生成当前项目所有类的测试代码,即默认全量生成。
  2. closeMock配置项是全局的Mock开关,设为true时所有Mock相关的代码都不会生成,包括Mock类和Test类中的Mock操作代码。
  3. 添加onlyIncludeMockPackageMethods配置 ,实现只对特定的依赖类和方法生成Mock代码,此时excludeMock前缀的配置项会被忽略。
  4. excludeMockMethodPrefixes和excludeMockMethodSuffixes配置项会对所有包和类中的方法进行匹配,而excludeMockPackageMethodPrefixes和excludeMockPackageMethodSuffixes只对“:”(英文冒号)之前所表示的包和类中的方法进行匹配,即前者是全量匹配后者是部分匹配。
  5. 可以在命令行中传入需要生成单元测试的类名或包名,例如mvn ut:generate -Dtarget=xxx  其中xxx为需要生成UT的包名、全类名、类名或者它们的前缀,多个名称用英文逗号隔开,这样就只会对符合条件的类生成UT。
  6. 可以在命令行中传入目标输出文件的路径,例如mvn ut:generate -DoutputPath=D\:test,代表生成的文件会放在D盘test文件夹中。可以结合target选项一起使用,例如mvn ut:generate -DoutputPath=D\:test -Dtarget=xxx。
  7. 当项目存在非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)来初始化被测对象,具体方式为:

  1. 在实例化被测类之前调用Mock文件中提供的构造器的Mock方法
  2. 实例化被测类得到被测实例,由于mock掉了构造器,被测实例的字段为未初始化状态
  3. 通过Mock文件中的getMockInstance方法获取需要初始化的字段类型的Mock对象
  4. 通过setField方法把第三步中得到的Mock对象设置到被测实例中

 

关于JMockit的更多细节请参考官方文档:Jmockit官方教程

 

你可能感兴趣的:(单元模拟测试--UT 自动生成+JMockit 工具)