Service层Mock测试 2018-05-15

(关键词:逻辑层(service)和数据访问层(Dao) Unitils、Spring测试框架、UnitilsJUnit4)

1 事件Service层单元测试

为了将项目与Jenkins结合做到更好的持续集成,我们需要单元测试覆盖到逻辑层(service)和数据访问层(Dao)。

(注:本项目数据访问层为Mapper层,以下Dao与Mapper互译。)

1.1 Service层开展单元测试的困境:

1. 业务逻辑复杂,分支繁多。不仅要构造正常的情况,还要测试异常的分支,这比Dao仅仅是几条sql就复杂多了。复杂的逻辑加上很多异常无法构造,一些关键的异常分支无法覆盖。

2. 数据库垂直切分的设计,Service层不得以操作了多个数据库,而连接多个数据库导致测试极慢,另外还因为涉及到跨数据库事务的难题,这个时候使用DBUnit来准备每个数据库的数据的方法已经不能适应了,整个数据库的环境是不稳定的。

3. Service层的Spring配置文件复杂,不仅包括了数据库的配置,还有其他框架配置(本项目中的dubbo)等等。启动测试就需要这些环境的配合,稍微一个不小心就会出现配置错误,整个测试失败。测试受环境影响,容易集成失败。

1.2 解决思路

我们不应该让Service层的单元测试依赖太多的东西,单元测试要体现“单元”的概念,不依赖数据库,不依赖其非必须的框架。根据这个原则,我们使用Mock对象,把service层用到的Dao等对象都一一的mock并插入到Service对象中。然后使用Unitils框架做模拟录制Dao(Mapper)的访问行为。这样就可以把Service的测试完全隔离开。经过处理后,Service的覆盖率和处理速度都得到了显著提升。

2 方案设计:

2.1 测试框架选型:Unitils、Spring测试框架、UnitilsJUnit4

2.1.1 Mock模拟对象——为交互而生:

单个的Junit4单元测试框架往往适用于以下场景的测试:单个函数,一个class,或者几个功能相关class的测试,对于纯函数测试,接口级别的测试尤其适用。例如Incident项目中 utils包中的日期、字符串等工具类的测试。

但是,对于以下的复杂场景:

 ①被测对象依赖复杂,甚至无法简单new出这个对象

 ②对于一些failure场景的测试

 ③被测对象中涉及多线程合作

 ④被测对象通过消息与外界交互的场景

 …

单纯依赖单测框架是无法实现单元测试的,而从某种意义上来说,这些场景反而是测试中的重点。

以分布式系统的测试为例,class 与 function级别的单元测试对整个系统的帮助不大,当然,这种单元测试对单个程序的质量有帮助;分布式系统测试的要点是测试业务间的交互。Mock方法的引入通常能帮助我们解决以上场景中遇到的难题。

Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。

引入Mock最大的优势在于:Mock的行为固定,它确保当你访问该Mock的某个方法时总是能够获得一个没有任何逻辑的直接就返回的预期结果。

Mock Object的使用通常会带来以下一些好处:

 ①隔绝其他模块出错引起本模块的测试错误。

 ②隔绝其他模块的开发状态只要定义好接口不用管他们开发有没有完成。

 ③一些速度较慢的操作,可以用Mock Object代替,快速返回。

 ④对于分布式系统的测试通过Mock Object可以将一些分布式测试转化为本地的测试。

2.1.2 Spring测试框架:

选用Spring测试框架理由如下:

测试环境运行在@RunWith(SpringJUnit4ClassRunner.class)下;

为了测试用例JavaBean的装配方便安全,我们项目中大量使用Spring注解;

业务范围内几乎所有的操作都是在事物环境下进行的,mock虽然为模拟对象,为了更加真实的模拟实际业务操作,我们也为测试用例配置Spring的事物管理,incident-Unitilstest.xml:

2.2 测试数据源:Json类型

Service层测试数据准备很麻烦,需要为每个Dao的返回对象做假数据。一般的String还好,返回JavaBean的就麻烦,而特别悲催是那种返回一个list的JavaBean接口,JavaBean还嵌套其他Bean,要一个个对象、属性的填塞。不行的是Dao的query函数往往都是返回这种List对象的,这样导致测试代码比开发工作量还大,而且很难维护。

于是我们希望和Dbunit一样,将数据的准备通过资源文件来完成,不用在测试代码中构造。JavaBean和Json之间互转的效率高,而且方便。所以我们将Dao的返回转换为Json字符串打印保存下来,存放为txt文件。然后在Service的测试中,在通过Unitils的IO能力,将文件内容读出为字符串,再转换为List/Bean的对象,放到Mock的Dao返回中。为了测试,我们准备了一个incidentBean的文件:incidentServiceTest2.txt

{

      "incidentId": "1",

      "incidentTypeId": 1,

      "subTypeId": 1,

      "levelId": "1",

      "businessTypeId": "1",

      "customerId": "1",

      "feedbackType": 0,

      "feedbackNumber": "13600000000",

      "dcId": "1",

      "simpleDescribe": "测试",

      "detailedDescribe": null,

      "status": null,

      "sourceId": null,

      "resourceId": null,

      "creatorId": null,

      "currentHandlerId": null,

      "createTime": "2017-03-07 09:51:39",

      "updateTime": "2017-03-07 09:51:39",

      "completeTime": null

}

注:测试源数据文件路径可以在测试中通过org.unitils.io.annotation.FileContent;中多种方式注解方式指定。本案例数据源文件放在默认的在测试用例相同的package下。

3 Unitils-mock测试案例详解

下面从是incident-service服务模块service中incidentService类提取的queryIncidentById源码:

首先分析其方法的调用逻辑如下:

这一步很重要,因为我们要mock的对象就是直接对数据库进行操作的Mapper,也是测试中隔离数据库操作的关键。如果随意把incidentDao设置为mock对象,那么回放mock行为时测试一定不通过,有返回结果的回放行为将报空指针异常。另外也是充分测试交互行为的需要。

测试用例编写如下:

@RunWith(SpringJUnit4ClassRunner.class) ①

@ContextConfiguration(locations={"incident-Unitilstest.xml"}) ②

public class IncidentServiceTest2 extends UnitilsJUnit4 ①

{

    private IncidentMapper incidentMapper; ③

    Incident incident; ④


    @Resource(name = "incidentDao")

    private IncidentDao incidentDao; ⑤


    @Resource(name = "incidentService")

    private IncidentService incidentService; ⑤


    @FileContent("IncidentServiceTest2.txt")

    private String incidentData; ⑥


    @Before

    public void init()

    {

        incidentData = IOUnitils.readFileContent(String.class, this); ⑦

        incident = JSON.parseObject(incidentData, Incident.class); ⑧

        incidentMapper = mock(IncidentMapper.class); ⑨

    }   

    @Test

    public void testQueryIncidentById()

    {       

        /*Incident incident = new Incident(); ⑩

        incident.setIncidentId("001");

        incident.setSimpleDescribe("测试");

        incident.setStatus(0);

        incident.setCreateTime(new Timestamp(System.currentTimeMillis()));

        incident.setCompleteTime(null);

        incident.setFeedbackNumber("13600000000");*/ ⑩

        //录制mock的行为

        doReturn(incident).when(incidentMapper).queryIncidentById("1");⑪

        //通过Spring测试框架提供的工具类为目标对象私有属性赋值 ⑫

        ReflectionTestUtils.setField(incidentDao,"incidentMapper",incidentMapper);

        ReflectionTestUtils.setField(incidentService,"incidentDao",incidentDao); ⑫

        //启用incidentService中queryIncidentById方法,回放行为

        Incident incidentresult = incidentService.queryIncidentById("1"); ⑬

        assertNotNull(incidentresult); ⑭

        assertThat(incidentresult.getFeedbackNumber(),

                equalTo("13600000000")); ⑮             

        //验证交互行为

        verify(incidentMapper, times(1)).queryIncidentById("1");⑯       

    }       

}

① @RunWith(SpringJUnit4ClassRunner.class)定义使用的测试框架SpringJUnit4;并且让测试类继承UnitilsJUnit4,便如执行时使用Junit4运行测试用例。这样做方便后面做集成测试需要,因为许多项目构建工具maven、gradle都完美支持Junit4插件。

② @ContextConfiguration加载配置事物的Spring文件:incident-Unitilstest.xml

③ 定义incidentMapper,作为后面mock的对象

④ 定义incident实体对象,作为后面json数据解析玩后装配的对象

⑤ 从Spring容器中加载incidentDao、incidentService的实例

⑥ @FileContent("IncidentServiceTest2.txt"),定义数据源文件路径,一遍unitils-io读取到指定的数据

⑦ IOUnitils.readFileContent(String.class, this),读取源数据

⑧ 将读取的源数据装配到incident对象中。

⑨ 创建incidentMapper模拟对象

⑩ 不使用json转化JavaBean类型时需要创建对象在一一为其属性设值(该代码块已被注释)

⑪ 录制mock的行为,也就是模拟incidentMapper去数据库中操作findIncidentById

⑫ 通过Spring测试框架提供的工具类ReflectionTestUtils.setField()为目标类私有属性赋值。ReflectionTestUtils.setField(incidentDao,"incidentMapper",incidentMapper)释义:incidentDao为目标类;“incidentMapper”为incidentDao目标类的一个私有属性。将incidentMapper赋值给incidentDao中的“incidentMapper”。

注意:按照逻辑顺序赋值

⑬ incidentService调用queryIncidentById方法,回放mock行为

⑭ 有结果的回放行为时,incidentService调用queryIncidentById时有真实的返还结果incidentresult。

⑮ 断言从回放结果incidentresult中取得的feedbackNumber属性与json源数据的一致

⑯ verify(incidentMapper, times(1)).queryIncidentById("1")验证交互行为有发生且仅发生1次。

Eclipse上运行该测试用例结果如下:

修改发生交互行为次数为2次时:

verify(incidentMapper, times(2)).queryIncidentById("1")再次运行测试,结果如下:

测试不通过,原因是交互行为实际只发生1次。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 总是把事情想的太简单,太美好。 所有人都反对的事,我却偏偏要做,现在就算后悔也来不及。 对未来充满了恐惧,做了整整...
    安一destiny阅读 204评论 0 0
  • 儿子小时候对音乐和节奏很敏感,为了早早开发儿子的音乐素养,我们和别的家长一样为儿子报了钢琴班,但天赋和吃苦...
    静心ling阅读 254评论 0 1
  • @刘学凤|007-3181|践行 【The title】How to improve education qual...
    我是刘荣阅读 197评论 0 0