Java单元测试实践-17.Mybatis与Mock

Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340

1. Mybatis与Mock

在对Mybatis的Mapper对象进行处理时,可能需要使某个Mapper对象在某些情况下返回指定值,在某些情况下执行真实方法;或者需要对Mapper对象相关的数据库操作进行记录,以下进行说明。

1.1. 测试示例说明

与Mybatis相关的测试类在测试示例的adrninistrator.test.testmock.mybatis包中,该包中的测试代码执行时需要连接MySQL数据库,由于示例配置文件中默认使用MockDriver作为数据源驱动,不会连接MySQL数据库,因此以上包中的测试代码不能直接执行。

在执行adrninistrator.test.testmock.mybatis包中的测试类时,可以使用gradlew/gradle命令,并指定“use_mysql”参数,unittest.gradle脚本在发现执行Gradle任务包含“use_mysql”参数时,会执行以上包中的测试类,并使用base_MySQL.properties配置文件覆盖base.properties,从而使测试程序连接MySQL;不包含以上参数时,不会执行以上包中的测试类。

使用gradle命令,并指定“use_mysql”参数的命令示例如下:

gradlew/gradle test -Puse_mysql

在进行以上测试时,需要确保base_MySQL.properties配置文件中的数据库参数对应的数据库服务可连接,并执行create-table.sql文件中的SQL语句创建测试代码使用的测试数据库表。

1.2. Mapper对象类名

Mybatis的Mapper对象使用了JDK动态代理,类名示例为“com.sun.proxy.$Proxy44”。可参考示例TestMybatisMapperInfo类。

1.3. 对Mapper对象进行Mock

1.3.1. 修改Mapper对象的Mock对象的返回值/抛出异常

对Mapper对象的Mock对象,可以进行Stub,修改指定方法的行为,修改返回值或抛出异常,示例如下:

TestTableMapper testTableMapper = Mockito.mock(TestTableMapper.class);

Mockito.when(testTableMapper.selectByPrimaryKey(Mockito.anyString())).thenReturn(new TestTableEntity());

可参考示例TestMockMybatisMapperReturn1、TestMockMybatisMapperReturn2、TestMockMybatisMapperThrows类。

1.3.2. 使Mapper对象的Mock对象执行真实方法

执行Mockito.mock()方法创建Mapper对象的Mock对象时,指定默认Answer为执行真实方法,执行Mock对象的方法时,不会执行真实方法。可参考示例TestMockMybatisMapperNotCallRealMethod类。

可对Mapper对象的Mock对象进行Stub,当请求参数满足条件时执行真实方法,可以在Mapper对象的Mock对象的Answer中执行原始的Mapper对象的方法,从而执行真实方法。示例如下:

@Autowired
private TestTableMapper testTableMapper;

String time = String.valueOf(System.currentTimeMillis());

TestTableMapper testTableMapperMock = Mockito.mock(TestTableMapper.class);

Mockito.when(testTableMapperMock.insert(
        Mockito.argThat(argument -> time.equals(argument.getId()) && time.equals(argument.getFlag()))))
        .thenAnswer(invocation -> {

            TestTableEntity arg1 = invocation.getArgument(0);

            return testTableMapper.insert(arg1);
        });

Mockito.when(testTableMapperMock.selectByPrimaryKey(time)).thenAnswer(invocation -> {

    String arg1 = invocation.getArgument(0);

    return testTableMapper.selectByPrimaryKey(arg1);
});        

可参考示例TestMockMybatisMapperCallRealMethod类。

1.4. 对Mapper对象进行Spy

1.4.1. Mapper对象不支持Spy操作

Mapper对象不支持Spy操作,对Mapper对象执行Mockito.spy操作时,会出现异常,示例及异常信息如下所示。

@Autowired
private TestTableMapper testTableMapper;

assertThrows(Exception.class, () ->
        Mockito.spy(testTableMapper)
);
org.mockito.exceptions.base.MockitoException
Cannot mock/spy class com.sun.proxy.$Proxy44
Mockito cannot mock/spy because :
 - final class

1.4.2. 对MapperProxy进行Spy

在Mapper对象中,包含类型为MapperProxy<T>(T为当前Mapper对象对应的接口),名称为“h”的对象。

org.apache.ibatis.binding.MapperProxy类在mybatis-xxx.jar中,当Mybatis进行数据库操作时,会执行MapperProxy.invoke()方法。

MapperProxy类中包含类型为Class<T>的变量mapperInterface,即MapperProxy类对应的Mapper对象的类型。

1.4.2.1. 对MapperProxy进行Spy的过程

可对MapperProxy对象进行Spy操作,再将Mapper对象中的MapperProxy替换为Spy对象,示例如下:

MapperProxy<TestTableMapper> mapperProxy = Whitebox.getInternalState(testTableMapper, "h");

proxySpy = Mockito.spy(mapperProxy);

Whitebox.setInternalState(testTableMapper, proxySpy);

以上处理过程可参考示例TestSpyMybatisMapperBase类。

1.4.2.2. MapperProxy.invoke()方法调用参数

MapperProxy.invoke()方法的参数为“Object proxy, Method method, Object[] args”。

对MapperProxy.invoke()方法进行Stub,在Answer中获取到的参数内容如下:

参数类型说明
参数1Object对象被调用的Mapper对象
参数2Method对象被调用的Mapper对象的方法
参数3Object数组调用Mapper对象的方法时传入的参数

通过invocation对象的相关方法可以获取上述参数,示例如下:

PowerMockito.doAnswer(invocation -> {

    Object object = invocation.getArgument(0);
    Method method = invocation.getArgument(1);
    Object[] objects = invocation.getArgument(2);

    // 参数1为Mapper对象
    assertEquals(testTableMapper.getClass(), object.getClass());

    // 参数2为被调用的Mapper对象的方法
    assertEquals("selectByPrimaryKey", method.getName());

    // 参数3为调用Mapper对象的方法时传入的参数
    assertEquals(1, objects.length);

    return ...;
}).when(proxySpy).invoke(Mockito.any(Object.class), Mockito.any(Method.class), Mockito.any(Object[].class));

可参考示例TestSpyMybatisMapperGetArgs类。

1.4.2.3. 支持的Stub操作

获取Mapper对象中的MapperProxy对象后,对其Spy对象的方法进行Stub,可在Answer中通过invocation.callRealMethod()执行真实方法,示例如下:

PowerMockito.doAnswer(invocation -> invocation.callRealMethod()).when(proxySpy).invoke(Mockito.any(Object
        .class), Mockito.any(Method.class), Mockito.any(Object[].class));

可参考示例TestSpyMybatisMapperCallRealMethod类。

还可在Answer中对调用参数进行检查。可参考示例TestSpyMybatisMapperCheckArgs类。

1.5. 对MapperProxy类的invoke()方法进行Replace

Mapper对象中的MapperProxy类的方法支持进行Replace处理。需要使用@PrepareForTest注解指定MapperProxy.class。

对MapperProxy.invoke()方法进行Replace,PowerMockito.replace().with()方法指定的InvocationHandler实例的参数为“Object proxy, Method method, Object[] args”,以上参数的说明如下。

参数类型说明
参数1Object对象MapperProxy对象
参数2Method对象被调用的MapperProxy对象的invoke方法
参数3Object数组与前文表格的参数说明相同

MapperProxy类中包含的变量mapperInterface,等于MapperProxy类对应的Mapper类型,示例如下:

PowerMockito.replace(PowerMockito.method(MapperProxy.class, "invoke")).with((proxy, method, args) -> {

    // 参数1为MapperProxy
    assertTrue(proxy instanceof MapperProxy);

    MapperProxy mapperProxy = (MapperProxy) proxy;

    Class<Object> mapperInterfaceClass = Whitebox.getInternalState(mapperProxy, "mapperInterface");

    // 参数2为invoke方法
    assertTrue(method instanceof Method);
    assertEquals("invoke", method.getName());

    // 参数3为调用参数列表
    assertEquals(3, args.length);

    // args的参数1为被调用的Mapper对象
    assertEquals(testTableMapper.getClass(), args[0].getClass());

    // args的参数2为被调用的Mapper对象的方法
    assertTrue(args[1] instanceof Method);
    Method method1 = (Method) args[1];

    // args的参数3为调用Mapper对象的方法时传入的参数列表
    assertTrue(args[2] instanceof Object[]);
    Object[] args2 = (Object[]) args[2];
    assertEquals(1, args2.length);

    return ...;
});

可参考示例TestReplaceMybatisMapperGetArgs类。

在PowerMockito.replace().with()方法指定的InvocationHandler实例中,可以根据调用参数决定执行真实方法(执行method.invoke()方法)或返回指定值,示例如下:

PowerMockito.replace(PowerMockito.method(MapperProxy.class, "invoke")).with((proxy, method, args) -> {

    MapperProxy mapperProxy = (MapperProxy) proxy;

    Class<Object> mapperInterfaceClass = Whitebox.getInternalState(mapperProxy, "mapperInterface");

    // 当不是TestTableMapper时,执行真实方法
    if (!TestTableMapper.class.equals(mapperInterfaceClass)) {
        return method.invoke(proxy, args);
    }

    // 其他情况返回指定值
    TestTableEntity testTableEntity = new TestTableEntity();
    ...

    return testTableEntity;
});

可参考示例TestReplaceMybatisMapperCallRealMethod类。

1.6. 对Mapper对象委托方法调用

将Mapper对象替换为方法调用被委托的Mock对象,可执行真实方法或返回指定值。

对Mapper对象委托方法调用可参考示例TestMockMybatisMapperDelegatesTo1、TestMybatisDelegate类,示例如下:

TestMybatisDelegate类为Spring的@Component组件,在test模块中(需要确保添加至Spring的包扫描路径中),可当作TestTableMapper的被委托代表使用,与TestTableMapper具有相同的方法,在TestMybatisDelegate中注入了TestTableMapper对象,可以根据需要执行真实方法,或返回指定的值,如下所示:

@Service
public class TestMybatisDelegate {

    @Autowired
    private TestTableMapper testTableMapper;

    private String time;

    public int deleteByPrimaryKey(String id) {
        return 0;
    }

    public int insert(TestTableEntity record) {
        if (record == null) {
            return 0;
        }

        if (time.equals(record.getId()) && time.equals(record.getFlag())) {
            return testTableMapper.insert(record);
        }

        return 0;
    }

    ...

    public void setTime(String time) {
        this.time = time;
    }
}

在TestMockMybatisMapperDelegatesTo1类中,注入了TestTableMapper的被委托代表TestMybatisDelegate的实例,在生成TestTableMapper的Mock对象时,使用TestMybatisDelegate实例作为其被委托代表,并将TestService3对象中的TestTableMapper对象替换为TestMybatisDelegate实例,如下所示:

@Autowired
private TestService3 testService3;

@Autowired
private TestMybatisDelegate testMybatisDelegate;

TestTableMapper testTableMapperMock = Mockito.mock(TestTableMapper.class, AdditionalAnswers.delegatesTo(testMybatisDelegate));

Whitebox.setInternalState(testService3, testTableMapperMock);

1.7. 在test模块使用mybatis-generator

在使用mybatis-generator时,可将对mybatis-generator的依赖添加至test模块中,在执行mybatis-generator时,指定test模块。

例如使用Gradle时添加至testImplementation中,可以避免添加至main模块后打包时包含mybatis-generator。

1.8. 在测试代码中使用Mybatis Mapper对象

可在测试代码中注入并使用Mybatis Mapper对象,可以直接测试Mybatis Mapper对象的方法,或者通过Mapper对象在测试开始前生成测试数据,或在测试结束后查询被测试代码生成的数据库记录是否符合预期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值