Fluent Mock 入门一

1,405 阅读4分钟

fluent mock是一款利用instrument(java agent)和 annotation processor这2个java提供的黑科技实现测试过程 中mock Java类/接口/对象的Mock工具。

fluent mock和主流的mock框架对比有什么不同,及存在的必要?

问题一:和市面上其它mock框架对比

市面上已经有各种形形色色的mock框架,知名的就有easy-mock、 jmock、 mockito、 power-mock、 jmockit, 各有绝招,为什么还要有fluent-mock呢?

首先大概直观的看下各色mock框架的招式和优缺点

框架原理mock对象mock范围Jacoco
Mockito动态代理对象,需先创建mock对象,然后对mock对象指定行为不能Mock私有/静态和构造方法兼容
PowerMock类加载器对象,需先创建mock对象,然后对mock对象指定行为任何方法不兼容
EasyMock动态代理对象,需先创建mock对象,然后对mock对象指定行为不能Mock私有/静态和构造方法兼容
jMock动态代理对象,需先创建mock对象,然后对mock对象指定行为不能Mock私有/静态和构造方法兼容
jMockitinstrument类,接口。修改类字节码,无需先创建mock对象,对所有该类实例生效任何方法(高版本限制了对private方法的mock)兼容
Fluent-Mockinstrument类,接口,对象。修改类字节码,无需先创建mock对象,可以对所有该类所有实例生效,也可以对指定的实例才生效。除native限定外的任何方法兼容

从技术体系上看,基本可以分成2类

  • 使用动态代理(或classloader) 主要是传统的mock框架: mockito, powermock, easymock, jmock等,因为技术特点的限制, 对mock行为不能随心所欲,主要限制有
    1. 不能对私有函数,final类,final函数mock外;
    2. 不能对构造函数,静态代码块进行mock
    3. 需要使用mock对象替换掉原来已经存在的对象,需要管理对象的依赖关系。对从原来类new出来的对象实例,没法指定行为。
  • 修改原类的字节码 jmockit和fluent-mock, 因为是修改既有类的代码逻辑,克服了动态代理方式的限制,任意对象进入要mock的方法, 执行的都是修改过的字节码逻辑,无需刻意维护(替换)原来的依赖关系。

问题二: 和jmockit框架对比

既然fluent-mock和jmockit采用的是同一个技术体系,jmockit也已经发展多年了,fluent-mock有什么特别的地方呢? 作者也是多年的jmockit的重度使用者,因为jmockit相对其它mock框架,来的直接和方便,特别是new MockUP和@Mock方式来进行mock。 多年的使用,觉的jmockit还是不尽如意,主要表现在:

  1. Expectations的语法比较鸡肋, 可能看法不一样,个人早期有使用,后面基本放弃这种用法。
  2. new MockUp的方式,虽然可以mock所有方法,但如果原有方法名称和参数做了重构,编译上没法发现。
  3. 早期的版本可以mock私有方法,后期版本把private方法mock给屏蔽了。
  4. new MockUp方式对所有给类的实例生效,没法做到只对指定实例生效(Expectations方式可以),但对其它实例无影响。
  5. 在指定mock行为的语法,较其它mock框架在流畅度上有点差距。

fluent-mock主要是致力于解决上述不足,吸收了jmockit框架的new MockUp + @Mock的特性,并且根据要mock的类元信息,动态编译生成mock行为指导方法。 充分利用fluent接口的特点,可以无记忆流畅的指定mock行为。 不仅可以普适性的mock,还可以针对性(对指定对象)mock。

来段代码,简单感受下fluent-mock的特性:

/**
 * 对MyServiceImpl这个类进行mock
 */
@Mocks({MyServiceImpl.class})
public class MyServiceImplTest {
    /** 这个类是AnnotationProcessor生成的, 可以给使用者提供fluent语法指导 **/
    private MyServiceImplTestMocks mocks = new MyServiceImplTestMocks();

    @DisplayName("验证fluent mock对指定实例进行mock")
    @Test
    public void test() {
        /** 无差别创建3个实例**/
        MyServiceImpl myService1 = new MyServiceImpl();
        MyServiceImpl myService2 = new MyServiceImpl();
        MyServiceImpl myService3 = new MyServiceImpl();
        
        /** 只对实例1和3进行行为指定, 放过实例2 **/
        mocks.MyServiceImpl(myService1).sayHello2.thenReturn("1");
        mocks.MyServiceImpl(myService3).sayHello2.thenReturn("3");
            
        String result1 = myService1.sayHello("m");
        String result2 = myService2.sayHello("m");
        String result3 = myService3.sayHello("m");
        /** 验证接口行为 **/
        Assertions.assertEquals("1", result1);
        Assertions.assertEquals("hello:m", result2);
        Assertions.assertEquals("3", result3);
    }
}

public class MyServiceImpl {
    public String sayHello(String name) {
        return "hello:" + name;
    }
}

fluent mock配置

maven配置

  • jar包引用 在项目pom.xml文件中
<project>
    <properties>
        <fluent-mock.version>1.0.4</fluent-mock.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.test4j</groupId>
            <artifactId>fluent-mock</artifactId>
            <version>${fluent-mock.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
  • 和surefire 和maven-surefire-plugin插件一起使用,需要在 argLine 参数配置参数
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <argLine>-javaagent:${settings.localRepository}/org/test4j/fluent-mock/${fluent-mock.version}/fluent-mock-${fluent-mock.version}.jar</argLine>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 和surefire + jacoco一起 如果同时使用了jacoco的来统计单元测试覆盖率,则需在配置中添加一个@{argLine}
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <argLine>@{argLine} -javaagent:${settings.localRepository}/org/test4j/fluent-mock/${fluent-mock.version}/fluent-mock-${fluent-mock.version}.jar</argLine>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.6</version>
            <executions>
                <execution>
                    <id>default-prepare-agent</id>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>default-report</id>
                    <goals>
                        <goal>report</goal>
                    </goals>
                    <phase>test</phase>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

gradle配置

dependencies {
    testCompile('org.test4j:fluent-mock:${fluent-mock.version}')
    //annotationProcessor('org.test4j:fluent-mock:${fluent-mock.version}')
    testAnnotationProcessor('org.test4j:fluent-mock:${fluent-mock.version}')

    test {
        jvmArgs "-javaagent:${classpath.find { it.name.contains("fluent-mock") }.absolutePath}"
        useJUnitPlatform()
    }
}

IDE配置

大部分情况下,IDE会自动识别maven和gradle的配置,如果mock框架没有起作用,可以在运行测试的jvm参数中加上:

-javaagent:{你本地仓库根路径}/org/test4j/fluent-mock/1.0.4/fluent-mock-1.0.4.jar

链接

Fluent Mock开源地址

Fluent Mybatis开源地址