测试持久层框架MyBatis代码

目录

一. 持久层框架MyBatis是什么

二.为什么要测试MyBatis代码

三.测试MyBatis代码的思路

四. 单元测试对MyBatis代码测试的流程

五. 对java对象和JDBC数据互相转换的说明

六. 剖析SQL的组装和进行验证

七. 对JDBC数据转换为java对象的说明

八. 测试映射

九. 对特殊情况的mapper方法的验证


一. 持久层框架MyBatis是什么

MyBatis是支持自定义SQL查询,存储过程和高级映射的持久层框架。它可以用XML或注解进行配置和映射,通过将参数映射到配置的SQL形成最终执行的SQL语句,最后将执行的结果映射成java对象返回。

简单的说,由于java代码不能直接使用关系型数据库的数据,需要一个映射的桥梁,而MyBatis框架就充当了这个角色。

二.为什么要测试MyBatis代码

与我的上一篇关于测试SQL的文章中的测试对象观点一样,开发在实现service层代码具体功能时一般会需要调用关系型数据库,如果此时用到了MyBatis框架,这个过程需要写一系列的MyBatis代码,所以开发人员写的这些代码是测试对象。

三.测试MyBatis代码的思路

首先,开发使用MyBatis框架的大致流程为

1定义一个全局配置文件,一个系统只需要一个。主要内容是要链接的数据库的信息、用户名、密码,以及要定义的mapper类XML文件的地址

2为每一个数据库表声明一个实体类,类中的属性与数据库表的字段一一对应。

3为每一个数据库表声明一个mapper接口,接口中的方法对应的是XML文件中操作该表的SQL命令

4为每一个数据库表配置一个mapper的XML文件,主要内容为使用mapper接口中的方法的select/update/delete标签语句,以及与这些语句相对应用于映射的语句。

上述流程是针对一个完整的新项目的。如果是在迭代过程中建新表,则省略第1步。如果是对一个表进行字段或其他修改,要看修改的范围,视需要分析是否需要进行2、3、4步的改动。清楚这些的流程会对测试MyBatis代码有非常好的指引作用。

再来看一下MyBatis框架运行的大致流程

1 java代码调用了某个mapper接口中的方法

2MyBatis框架找到与该mapper接口同名的XML文件,获取此文件中与1的方法同名的select/update/delete标签语句(这些标签语句会有一个id)

3根据MyBatis的全局配置文件连接数据库并运行SQL

4根据2中的语句的resultType/resultMap来对SQL的返回进行映射

开发使用MyBatis框架和MyBatis框架运行的流程能够指导我们获得对MyBatis代码进行测试的思路。

1)测试入口

mapper接口中的方法调用一般都是service层代码中的一部分,这与web应用可以从control层来测试service层代码不同,想要单独测试MyBatis代码需要通过编写测试代码来进行,也就是俗称的单元测试。

2)单独测试MyBatis代码

这里应该会有人产生疑问,为什么不能直接像测试接口一样直接用接口工具测试MyBatis代码呢,并不是测不到啊?回答是如果从接口工具测试,那测试时必然是会把control层、service层以及service层中的MyBatis代码三者混在一起测试的,如果只想测试MyBatis代码却从接口入手,势必会产生更大的开销。所以这里根据分层测试的理念,如果只想MyBatis代码,那就使用单元测试框架来做。

3)测试MyBatis代码要测试什么

所谓测试什么就是验证什么,现在已经根据上文得知了MyBatis框架的运行流程,开发使用MyBatis代码时哪些代码是需要他们编写的,以及我们想要实现测试时把MyBatis代码从service层代码中分离,这些信息和目标就能够指引我们得出答案。

4)对3)的回答

*验证SQL的组装

*验证SQL与java的映射

再多说一点,引用我上一篇SQL的文章中的观点,我们只关注开发有没有用对SQL语句,而不关注数据库会不会执行出错。

这句话其实就表达了从页面或者接口测试使用MyBatis代码的思路,而这里,我们要开始关注开发有没有用对SQL了。(用对SQL可以解释为符合需求,也可以理解为SQL命令能够运行,对于上一篇文章来说确切的解释是前者,这篇是后者)

四. 单元测试对MyBatis代码测试的流程

1对每一个接口类创建一个对应的测试类,对接口类下的每一个方法创建一个对应的测试方法

2为要调用的数据库表创建数据。

3在测试类中调用接口类下的方法,并获取数据库的返回

4使用断言验证返回和预期值,被映射的java对象

SQL语句的组装是否正确的标准很简单,运行时数据库不报错即可。验证映射则需要对SQL的返回的每个字段都进行断言,确保没有映射遗漏或映射错误。稍微分析一下就可以发现,映射成功的前提是SQL组装正确,所以从编写测试脚本的角度来说,不用单独去断言SQL语句了。(断言SQL语句的组装实际上需要用到SQL命令的规则集,这明显是超出测试范围了)

用一个简单的例子演示测试的流程

接口方法: UserMapper

 

对应的实体类: SysRoles

对应的XML

最后的测试脚本

public class BaseMapperTest {
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init(){
        try{
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();
        } catch (IOException ignore) {
            ignore.printStackTrace();
        }
        }

    public SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();
    }
    }
public class UserMapperTest extends BaseMapperTest {

    @Test
    public void testSelectById(){
        SqlSession sqlSession = getSqlSession();
        try{
            //获取对应的接口类
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //调用接口的方法查询数据
            SysUser user = userMapper.selectById(1L);
            //断言返回
            Assert.assertEquals("admin",user.getUserName());
            Assert.assertTrue("create_time是时间戳",user.getCreateTime() instanceof Date);
        }finally {
            sqlSession.close();
        }
    }

五. 对java对象和JDBC数据互相转换的说明

java中提供了一组名称为JDBC的API,用于连接和操作各种关系型数据库。而MyBatis框架也在内部使用了JDBC,所以与java对象进行映射的其实是JDBC数据类型。二者之间会互相发生类型的转换,它们的应用场景如下:

*java对象转换为JDBC数据

Mapper接口中的方法的入参,对应到

1)select或update或delete语句中where子句筛选项的值

2)update语句中set子句的值

3)insert语句中values子句的值

这一部分的功能就是动态SQL和使用mapper方法入参的SQL,会在第六部分做具体说明。

*JDBC数据转换为java对象

1)select语句的返回值,使用resultType或resultMap这2个属性用于映射,这是MyBatis代码编写中最复杂的部分之一,也是测试的重点。

2)与select语句不同,执行update/delete/insert语句后,数据库的返回是被影响的数据的数量,如果不需要使用数量这个变量可以把对应的方法的返回值设置为void。

所谓的验证SQL与java的映射就是这一部分的逻辑,会在第七和第八部分做具体说明。

六. 剖析SQL的组装和进行验证

第四部分举例的SQL中,筛选项的值使用了mapper方法中的入参,实际上,框架允许SQL语句在select子句、update的set子句、insert的value部分都使用方法中的入参来进行动态处理,不仅仅局限于where子句中。灵活性带来的是入参为空时可能导致的bug。

where子句中筛选项的值为null会导致全体筛选项失效,返回查询结果为0。因为SQL会被组装成筛选项=null,而使用null的正确语法是is null或is not null。update和delete语句也是一样的,会导致执行后被影响的数据是0条。

update语句中的set子句的值可以使用等于null的写法,所以update语句的set语句如果使用了入参却不是动态SQL,一定要按照需求来验证是否允许更新为null。

       上述情况我把它们分类为使用mapper方法入参却不使用动态SQL,这类SQL在做单元测试的时候去验证空值是没有意义的,必须要在集成测试/系统测试的层面去验证是否会出现因为方法入参为空导致bug。

       MyBatis框架的招牌功能动态SQL很大程度上就解决了上述问题,各个标签通过判断表达式可以实现如果参数为空则标签中的代码不执行,这样就不会出现筛选项=null或set=null这样的情况发生。

动态SQL的实现是从mapper方法的入参获取一个或多个值来用作test属性的值或where子句中的筛选项的值。

1)where标签和if标签

 

图中是把动态SQL中的where和if标签混合使用的简单例子,if标签的运行方式与编程语言中的if没有区别,通过test这个属性指定一个OGNL表达式,返回true就执行代码块中的代码,返回false就跳过。

where标签用于把SQL语句中的where子句部分动态化,和if标签配合使用就能实现按照需求对where子句中的筛选条件进行区别处理的需要。

测试if标签和对代码中的if语句进行白盒测试的方法相同,测试经过执行和不执行2条路径时SQL都能正常执行即可。

测试where标签的思路则是

*验证where标签内的if标签都返回true时,SQL能否正常执行

*验证where标签内的if标签都返回false时,SQL能否正常执行

第1条应该很好理解,第2条则是因为如果where标签内的if标签都返回false时,此时如果where标签中没有其他SQL语句,这一整段都会被忽略,这样的处理逻辑其实也提供给了开发者能够编写where子句时存在一定要执行的筛选逻辑。仔细看上图where标签的下方有user_password is not null这个不在2个if标签中的筛选条件,意思是即使2个if标签都是false,那么这个SQL执行时也会使用这个筛选逻辑。

此时,这个SQL会被组装为这样:

select id, user_name userName, user_email userEmail

from sys_user

WHERE user_password is not null

因此,结论是因为where标签提供了这样的处理逻辑,所以要对这个场景进行测试。

2)choose标签

choose标签为动态SQL提供了if-else的逻辑处理能力,用when和otherwise标签来实现,测试的方法和SQL中的case表达式是一样的,在测试每条路径之后,同样也要测试没有otherwise但when的条件都不满足的场景。

 

3)foreach标签

此标签实现了SQL中谓词in(?,?,?)的集合,测试点有2个

*对应的集合设置为不允许非空时,验证有2个元素时SQL运行正常

*对应的集合设置为允许非空,验证0个元素和2个元素时SQL运行正常。(有时候会把foreach标签放在if标签中,判断集合不为空才执行)

 

4) include标签

这个标签提供了把同一个XML文件内的多个SQL语句中的公共部分独立成块,然后使用include标签和refid属性来引用的功能。(概念上和把一部分代码封装为一个方法供其他代码调用没有区别)

被封装的SQL语句块存在于SQL标签中,并有一个id属性。这部分的SQL语句块由于没有对应的mapper接口,是不能从接口调用来直接测试的,这是和代码中封装的方法有区别的地方。(一般这部分SQL也不是完整的SQL语句)

使用了include标签的SQL语句的测试取决于引用方和被引用方的改动。

*引用方和被引用方都是新编写的代码,直接测试引用方即可

*有多个引用方引用了同一个SQL标签内的语句块,只有1个引用方被改动了,此时只需要测试这个引用方

*同上,如果改动了一个引用方和被引用方,那么所有的引用方都要测试一遍。

include标签其实也可以调用另一个XML文件中的SQL语句,测试的思路是一样的,但无疑这是大大增加代码复杂度的写法。

总结,验证动态SQL和白盒测试中的路径验证很相似,需要通过控制入参来验证每条路径上的SQL都是正常的。

七. 对JDBC数据转换为java对象的说明

Select语句由接口方法名称和映射标记组成,映射标记又分为resultType和resultMap 2类。有返参的mapper方法必然需要映射为java对象,有resultType和resultMap这2个属性用于映射。顾名思义,它的作用是把SQL命令的查询出的每行中的字段的值映射为java对象,而标记则用来指定是什么样的java对象。

mapper方法的返参类型

XML中SQL语句的映射类型

java基本类型(数字,字符串,集合)

resultType

自定义实体类

resultType /resultMap

当mapper方法的返参类型是基本类型时,一般select语句只查询1个或少数几个值,这样的写法一般供接口代码中逻辑判断使用。

自定义实体类可以是数据库表对应的实体类,也可以是DTO或response类,此时select语句会查询一个或多个表的多个值并映射为实体类。

映射为基本类型返回的例子

作为DTO返回的例子

        resultMap同样用于映射实体类,但实现逻辑更加复杂。设计上,如果一个模块对外只提供一个查询功能,那么不管这个模块有几个数据库表的数据,编写一个统一的DTO就能满足需求,此时就会使用resultType。如果一个模块有多个类组成,且提供多个查询功能,这些功能还需要互相关联,使用嵌套查询,一对多映射,那么就需要使用resultMap。如果select标签的语句使用了resultMap,那么具体映射为了哪个java对象要去同文件对应resultMap标签的type属性获取。

 

上图的例子中,这个id是ExtMap的resultMap的type是com.glp.abs.dto.AssetInfoDTO,就是要映射的java对象的名称。result标签中的column和property属性用于对数据库表的字段和实体类的属性进行映射。association标签则用于指定和这个resultMap进行一对多关联的另一个resultMap。

使用association或collection标签进行关联时,这2个标签的property属性的值对应的属性一定会写在对应的实体类中。

 

说明白resultType和resultMap 之后,我们就能够看到要如何测试映射了,如果是resultType映射为了基本数据类型,那么很方便,获取方法的返回直接和预期值断言即可。如果是映射为了实体类,情况要复杂一些,也是测试的另一个重点,请看下文。

八. 测试映射

编写resultMap相关的代码时,可能会出现以下问题:

1实体类属性的数据类型、resultMap中的对应字段的JDBC数据类型、数据库表字段的数据类型三者不一致。这里的不一致不仅仅是指不小心把字符串映射为了数字,还包括了JDBC数据类型转换为java对象时如果错误指定了数据类型会产生精度问题。二者的对应关系在MyBatis的源代码的type.TypeHandlerRegistry可以找到。

2 XML文件resultMap标签中属性column和property导致的错误

*column或property拼写错误,和正确的值对不上

*resultMap中的属性column和property和对应的实体类的属性相比有遗漏,或对应关系错误

虽然现在开发同事一般都会使用MBG工具(MyBatis框架自带的一个工具)来自动生成XML中的resultMap,但如果开发过程中需求和表的设计发生频繁改动,使得MyBatis相关代码也要修改,那么这类的错误发生的概率就大幅度增加了。同时,如果表中存在的一些名称或意思相近的字段(如果一个表的字段很多,这种情况是很常见的) 也会增加这类问题发生的概率。

测试映射的大致步骤如下:

1对要测试的mapper类对应的表插入数据,每个字段都要有值,且要能够互相区分。

2调用要测试的mapper类的select方法,使用插入的每个值来进行断言,验证映射是否正确。

插入数据时如果每个字段都有值且能够互相区分,就可以预防上述错误。

九. 对特殊情况的mapper方法的验证

1) 没有入参的mapper方法

重复一遍,mapper方法的入参用在XML文件SQL语句的2个地方,一个是在test属性中作为判断OGNL表达式的值,另一个是作为筛选项的值。方法没有入参意味着这个SQL语句不是动态SQL,where子句的筛选逻辑也是固定的,这类代码验证时直接调用即可。

2) 没有返参的mapper方法

mapper方法没有返参,这类情况一般只会出现在update/delete/insert语句中,这三类语句执行后,数据库会返回被影响的数据的数量,MyBatis框架能够在XML文件中定义一些属性来获得被更新/插入的数据的主键,获取数据的主键会使得写测试代码做断言非常的方便,但如果代码没有给我们留下这样的口子,又不想修改XML文件,就要另想办法。

*如果要测试的update/delete/insert的同一个Mapper接口有可以用的select的方法,那就先update,再调用那个select方法来验证

*如果没有,可以使用通用mapper包的example类来直接组装一个select语句来断言

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值