Fluent Mock入门三:走上fluent之路

902 阅读4分钟

使用new MockUp功能可以比较容易的对具体的方法进行改写,但也存在不方便的地方。

  1. 需要到具体实现类上,拷贝要mock的方法签名;而且如果方法名称或参数变化了,在MockUp里面并不能检测出来。
  2. 无法指定校验调用次数

在Fluent Mock框架中引入一个新的注解 @Mocks,可以很方便的解决上述问题。 对上篇文章中的mock示例, 我们用 @Mocks来实现一遍,就能对比出2者的差异。

各种限定类型方法mock演示

对静态代码块的mock

我们先建立一个测试类,并在类上定义@Mocks,value值是要mock的类列表

@Mocks(AnNormalService.class)
public class AnNormalServiceFluentMockUpTest {
    // ...
}

编译该类, AnnotationProcessor会在target/generated-test-sources目录下生成2个文件

  • 在@Mocks注解里面声明的需mock类快捷定义类
  1. 对应的类名称+MockUp后缀的MockUp列表,这些类路径是被mock类路径加前缀"mock."。
  2. 在示例中是 AnNormalServiceMockUp.java
  3. 这些类继承自FluentMockUp类,里面定义了对每一个方法进行mock的快捷方法。
  • *MockUp引导类
  1. 声明@Mocks类类名+Mocks后缀的类,这个类路径和声明类路径一样。
  2. 在示例中是 AnNormalServiceFluentMockUpTestMocks.java
  3. 可以在Test中声明,方便引用需要mock的类
@Mocks(AnNormalService.class)
public class AnNormalServiceFluentMockUpTest {
    AnNormalServiceFluentMockUpTestMocks mocks = new AnNormalServiceFluentMockUpTestMocks();

    // ...
}

各种限定类型方法mock演示

演示对public和final方法mock

@Mocks(AnNormalService.class)
public class AnNormalServiceFluentMockUpTest {
    AnNormalServiceFluentMockUpTestMocks mocks = new AnNormalServiceFluentMockUpTestMocks();

    @DisplayName("演示对public和final方法mock")
    @Test
    void test3() {
        mocks.AnNormalService().publicMethod.restReturn(5);
        mocks.AnNormalService().finalMethod.restReturn(6);

        AnNormalService service = new AnNormalService(400);
        Assertions.assertEquals(5, service.publicMethod());
        Assertions.assertEquals(6, service.finalMethod());
    }
}

示例中mock了AnNormalService2个方法:

  • mock方法publicMethod, 始终返回模拟值5
  • mock方法finalMethod, 始终返回模拟值6

[对应MockUp的例子](mockup-advanced.md#mock public和final方法)

FluentMockUp提供了3种行为模拟API

  • 直接返回值
    1. thenReturn(Object... ), 按声明顺序和调用顺序匹配方法,逐次返回模拟值。
    2. restReturn(Object), 除了指定的then行为外,剩下的调用都返回指定模拟obj值。
  • 抛出异常
    1. thenThrows(Throwable...), 按声明顺序和调用顺序匹配方法,逐次抛出异常。
    2. restThrows(Throwable), 除了指定的then行为外,剩下的调用都抛出指定模拟异常。
  • 定制行为
    1. thenAnswer(FakeFunction...), 按声明顺序和调用顺序匹配方法,逐次模拟行为。
    2. restAnswer(FakeFunction), 除了指定的then行为外,剩下的调用都执行的模拟行为。

演示对静态方法的mock

@Mocks(AnNormalService.class)
public class AnNormalServiceFluentMockUpTest {
    AnNormalServiceFluentMockUpTestMocks mocks = new AnNormalServiceFluentMockUpTestMocks();
  
    @DisplayName("演示对静态方法的mock")
    @Test
    void test5() {
        mocks.AnNormalService().getStaticInt
            .thenAnswer(inv -> {
                return (int) inv.proceed() + 4;
            })
            .thenAnswer(inv -> {
                return (int) inv.proceed() + 100;
            })
            .restAnswer(inv -> {
                return (int) inv.proceed() * 100;
            });
        AnNormalService.setStaticInt(2);
        Assertions.assertEquals(6, AnNormalService.getStaticInt());
        Assertions.assertEquals(102, AnNormalService.getStaticInt());
        Assertions.assertEquals(200, AnNormalService.getStaticInt());
        Assertions.assertEquals(200, AnNormalService.getStaticInt());
    }
}

示例中:

  1. 第一次调用模拟逻辑: 原接口返回值的 + 4, 这里期望值是6。
  2. 第二次调用模拟逻辑: 原接口返回值 + 100, 这里期望值是102。
  3. 剩余的调用模拟逻辑: 原接口返回值 * 100, 这里期望值是200。

thenAnswer和restAnswer入参是接口函数 FakeFunction

@FunctionalInterface
public interface FakeFunction<R> extends MockBehavior {
    /**
     * 定义mock方法实现
     *
     * @param it mock method proxy
     * @return
     */
    R doFake(Invocation it);
}

Invocation的详细解释

对方法入参进行断言

在thenAnswer和restAnswer中,可以通过Invocation对象获得入参,并对入参进行断言判断。

@Mocks(InvocationFluentMockDemo.Service.class)
public class InvocationFluentMockDemo {
    InvocationFluentMockDemoMocks mocks = new InvocationFluentMockDemoMocks();

    @DisplayName("对方法入参进行断言演示")
    @Test
    void test3() {
        mocks.Service().setString.restAnswer(inv -> {
            Assertions.assertEquals("期望值", inv.arg(0));
            return inv.proceed();
        });
        Service service = new Service();
        // 正常通过
        service.setString("期望值");
        // 设置其它值, 应该是抛出一个断言异常
        Assertions.assertThrows(AssertionError.class, () -> service.setString("其它值"));
    }
}
  1. inv.args()返回 Object[] args入参数组
  2. inv.arg(int index)返回第index个入参, index从0开始

对方法执行结果断言


@Mocks(InvocationFluentMockDemo.Service.class)
public class InvocationFluentMockDemo {
    InvocationFluentMockDemoMocks mocks = new InvocationFluentMockDemoMocks();

    @DisplayName("对方法出参进行断言演示")
    @Test
    void test4() {
        mocks.Service().getString.restAnswer(inv -> {
            String result = inv.proceed();
            Assertions.assertEquals("origin string", result);
            return result;
        });
        Service service = new Service();
        /**
         * 方法调用正常通过
         */
        service.getString();
        /**
         * 将返回值改为其它值, 方法调用应该抛出断言异常
         */
        service.setString("other value");
        Assertions.assertThrows(AssertionError.class, () -> service.getString());
    }
}

对所有的调用参数进行断言

语法: .assertParameters(inv->{ 具体断言 }) 示例:

@Mocks(MyServiceImpl.class)
public class AssertAroundTest {
    AssertAroundTestMocks mocks = new AssertAroundTestMocks();

    @Test
    void test_assertBefore() {
        mocks.MyServiceImpl().sayHello
            .assertParameters(invocation -> {
                String name = invocation.arg(0);
                Assertions.assertEquals("are u ok!", name);
            });
        new MyServiceImpl<>().sayHello("are u ok!");
    }
}

assertParameters方法会对指定方法的所有调用进行判断,不管是指定了mock逻辑,还是执行原方法逻辑。 上面的示例,就没有指定sayHello的模拟逻辑,但同样会进行拦截,然后做参数校验。

对执行结果进行校验

语法: assertResult((inv,result)->{}) 示例:

@Mocks(MyServiceImpl.class)
public class AssertAroundTest {
    AssertAroundTestMocks mocks = new AssertAroundTestMocks();

    @Test
    void test_assertAfter() {
        mocks.MyServiceImpl().sayHello
            .assertResult((inv, result) -> {
                Assertions.assertEquals("hello:are u ok!", result);
            });
        new MyServiceImpl<>().sayHello("are u ok!");
    }
}

链接

示例代码

FLuent Mock