• 9
  • 9
分享

如何正确编写单元测试?

国内的大多数互联网公司只注重软件功能,却往往忽略了极为重要的软件质量,在一个月以前,我认为遵循了代码规范(阿里规约、sonar)的软件系统已经算是一个质量比较好的软件系统了,但是在我了解单元测试以后,才发现自己以前的想法有多么愚蠢,单元测试的作用远比我想象的要重要许多。经过一段时间的研究,总算对单元测试有了一个大概的了解,然而网上的文章零零散散,大多是讲解一些比较简单的demo,参考价值比较有限,因此我决定写一篇关于单元测试的文章来总结自己这段时间的收获与心得。

背景

软件系统刚开发完成时几乎不会出现Bug。为什么呢?因为刚开发第一版软件系统时,需求并不复杂,场景也不是很多,因此实现起来比较简单,再加上测试小哥哥/小姐姐保驾护航,基本不会出现比较严重的bug。但是随着时间的推移,系统功能越加越多,需求越来越复杂,既要兼容原来的功能完好无损、又要保证新增的功能正常使用,再加上项目工期的不断逼近,导致开发小哥压力山大,于是心理历程逐渐转变为:代码和人只要有一个能跑就行的诡异心理。如果产品需求再模糊不清、频繁修改,估计开发小哥想死的心都有了 。与此同时,测试小哥也同样不轻松,因为他发现每次发布新功能竟然有可能会影响到另一个毫不相关的功能,为了保证每次发布新功能时不影响原有功能,于是不得不将原有功能进行回归测试,这无疑给测试小哥增加了成倍的工作量,久而久之,这个系统被越来越多的人厌烦,最后当大家都不愿意再维护这个系统时,这个系统也就走到了终点。即使最后想要重构,也会感觉无从下手,因为你无法预估代码变更所带来的的风险。

测试金字塔

针对上述问题,业界有一套公认的指导方案——测试金字塔。它将测试步骤分为多个层次,每个层次关注不同的测试内容,对于层次的划分,网上有很多种方式,但无一例外,它们最底层都是单元测试,由此可见,编写单元测试是多么的重要。随着对单元测试的不断了解,相关问题也随之而来:应该怎样编写单元测试?哪些代码需要编写单元测试?怎样评判单元测试的好坏?怎样规范的编写单元测试?单元测试的能够带来的好处有哪些?下面让我们一起来了解一下单元测试的爱恨情仇。

1.png

单元测试Demo

首先大致介绍一下该项目的背景,我们公司最近正在开发一个很小的功能,因为某些原因不得不拆分为一个独立项目进行开发,而我就是这个项目的开发人员,由于领导强烈要求80%的单元测试覆盖率以满足SonarQube的标准,所以我不得不花点时间去研究它。由于这个项目比较小,所以我就直接拿来当案例使用了(删除了一些敏感信息)。
项目技术栈:SpringBoot、JUnit4、mysql、Redis、mybatis-plus、Mockito
项目案例开源地址:https://gitee.com/hechaoqi123/unit-test.git

JUnit4的基础用法

JUnit是一个Java语言的单元测试框架,应用之广泛应该能够与Spring相媲美了吧。据我了解JUnit有两个广泛流传的版本,分别是JUnit4与Junit5,这两个版本的用法存在着很多差异,因此不建议混合使用,SpringBoot框架中已经默认支持了JUnit作为测试框架。因为我最先接触的是JUnit4版本,因此下文以JUnit4进行示例。

示例代码

public class DesensitizationUtil {
    public static String len11mobile(String mobile){
        String first = mobile.substring(0, 2);
        String last = mobile.substring(mobile.length()-4);
        return first+"****"+last;
    }
}

代码分析

这是一个非常简单的工具类,其功能是做手机号的脱敏处理,现在需要编写这个方法的单元测试,首先让我们分析一下单元测试的目的有哪些?

  • 我们希望单元测试可以验证这个方法的功能是否正常。

  • 我们希望单元测试可以将这个方法的所有情况全部验证,而不仅仅是某一个特定的条件

  • 当我们需要更改这个方法的实现细节时,单元测试可以帮助我们验证这次变更是否正确。

针对以上几点,我编写了如下的单元测试

单元测试

public class DesensitizationUtilTest {
    @Test
    public void testLen11mobile() {
        String mobile = "123456789";
        Assert.assertEquals(DesensitizationUtil.len11mobile(mobile),"12****6789");
    }
}
  • 当len11mobile()方法发生变化而被破坏时,该测试用例可以检测出其返回结果与期望值不匹配,从而进行风险提示

  • 上述例子只存在一个条件分支,因此只需要编写这一个测试用例就可以完全覆盖len11mobile()方法了。

  • 当我们需要修改此方法的内部实现时,如果该测试用例通过,则说明本次变更没有更改此方法的行为,因此便不会导致其他功能受其影响。在系统重构时,这一点尤为重要

Mockito的基础用法

上述例子仅仅完成了一个及其普通的单元测试,但是我们大多数的业务场景往往不那么简单,我们可能需要查询数据库、可能需要调用三方接口、也可能需要依赖其他组件(redis、mq)等等。这个时候我们面临的第一个问题就出来了:如何在单元测试中屏蔽掉这些外来因素的影响?于是Mockito被引入进来,使用Mockito,我们可以模拟一些对象的行为使其返回特定的数据。再说白一点就是Mockito会在运行单元测试时生成指定对象的代理对象,从而跳过真实的业务逻辑并返回我们预先设定好的数据类型(如果不理解的话建议先动手写个Demo,相信你会有更深刻的理解)。

示例代码

@AllArgsConstructor
public class UserServiceImpl implements UserService {
 
    private final UserMasterMapper userMasterMapper;
    
    @Override
    public boolean markMerchant(MarkMerchantModel model) {
        // 查询用户信息
        UserMasterEntity user = userMasterMapper.selectOne(
                Wrappers.<UserMasterEntity>lambdaQuery().eq(UserMasterEntity::getUserId,model.getUserId()));
        // 校验用户是否存在
        ExceptionAssertEnum.USER_NOT_EXIST.notNull(user);
        // 将其标记为商家类型
        user.setIsShopMerchant(model.isMarkMerchant());
        user.setShopMerchantDate(LocalDateTime.now());
        // 更新数据库
        int count = userMasterMapper.updateById(user);
        // 检验是否更新成功
        ExceptionAssertEnum.SYSTEM_EXCEPTION.isTrue(count == 1);
        // 返回业务结果
        return true;
    }
}

代码分析

这是一个较为简单的业务方法,该方法的功能是将用户标记为商家类型,为了使大家看起来更方便一些,我将每行代码都加了注释,大家可以看到这个方法其实存在多种不同的行为:

  • 当业务执行成功时返回true

  • 当数据库查询不到用户信息时抛出:USER_NOT_EXIST异常

  • 当数据库写入失败时抛出:SYSTEM_EXCEPTION异常

以上的几种行为便是单元测试所需要验证的内容,然而这些行为的验证都离不开DB的支持,因此我们需要通过Mock跳过DB操作,于是编写了如下的单元测试

单元测试

public abstract class BaseTest {
    @Before
    public void before() {
        MockitoAnnotations.openMocks(this);
    }
    public void assertThrows(ThrowingRunnable runnable, Integer errorCode) {
        BusinessException e = Assert.assertThrows(BusinessException.class, runnable);
        Assert.assertEquals(errorCode,e.getCode());
    }
}
 
public class UserServiceImplTest extends BaseTest {
 
    @Mock
    private UserMasterMapper userMasterMapper;
    @InjectMocks
    private UserServiceImpl service;
 
    @Test 
    public void testMarkMerchant() {
        MarkMerchantModel model = new MarkMerchantModel();
        model.setUserId("RealUserId");
        model.setMarkMerchant(true);
        when(userMasterMapper.selectOne(any())).thenReturn(new UserMasterEntity());
        when(userMasterMapper.updateById(any())).thenReturn(1);
        Assert.assertTrue(service.markMerchant(model));
    }
 
    @Test
    public void testMarkMerchantForUserNotExistException() {
        MarkMerchantModel model = new MarkMerchantModel();
        model.setUserId("testUserId");
        assertThrows(() -> service.markMerchant(model), ExceptionAssertEnum.USER_NOT_EXIST.getCode());
    }
 
    @Test
    public void testMarkMerchantUpdateException() {
        MarkMerchantModel model = new MarkMerchantModel();
        model.setUserId("RealUserId");
        model.setMarkMerchant(true);
        when(userMasterMapper.selectOne(any())).thenReturn(new UserMasterEntity());
        when(userMasterMapper.updateById(any())).thenReturn(0);
        assertThrows(() -> service.markMerchant(model), ExceptionAssertEnum.SYSTEM_EXCEPTION.getCode());
    }
 }
根据方法名称我想大家应该也可以猜得到这三个测试用例分别是对应以上三种行为。这里继承了BaseTest,因为我喜欢在父类中编写一些公共的方法。而@Before标注的方法会重复执行在每一个测试用例之前,MockitoAnnotations.openMocks(this)方法代表开启Mockito的注解功能,@Mock注解可以生成一个UserMasterMapper的代理对象,@InjectMocks注解可以将@Mock生成代理对象注入到serivce中,最后在具体的测试用例中通过when()设置不同的返回数据,从而完成UserMasterMapper对象的模拟,然后通过Assert验证该方法的行为是否符合预期,从而决定了单元测试的成功与否。

Mockito的用法其实还有很多,我没有一一叙述,因为相对于基础教学之类的文章,我更喜欢写一些能够传递我的思想观点的文章。

针对单元测试产生的疑问?

单元测试的目的?

代码变更时保证软件系统原有功能不被破坏。

单元测试的粒度?

我认为单元测试的粒度应该精确到类中的某个具体方法。

单元测试的覆盖率?

我们之所以编写单元测试,是为了保证业务代码的可靠运行。盲目追求100%的测试覆盖率并不会给我们带来质量上的提升,反而会加重我们的负担。所以不要为了测试覆盖率而编写单元测试。

单元测试的覆盖范围?

类覆盖、方法覆盖、行覆盖、条件覆盖。我认为条件覆盖是最为苛刻的一种,因为它需要输入不同的条件进行测试

哪些代码需要单元测试?

非常简单的方法(get、set、equals…)以及不对外暴露的方法(private…)无须编写单元测试

单元测试是否需要被测方法同步更新?

单元测试只关注被测方法的行为(参数、返回值),而不应该关注其实现细节。。

单元测试是否需要依赖Spring环境?

单元测试不需要依赖Spring环境,我更愿意将需要依赖Spring特性(Aop)的单元测试理解为一种狭义的集成测试。

单元测试是否需要依赖外部系统或中间件?

每一个开发人员都需要能够在本地反复的执行单元测试,所以单元测试不建议依赖任何的外部因素,这些因素都可能导致单元测试的失败,包括mysql、nacos、seate、redis、openFeign、三方接口等。这些因素需要在单元测试阶段进行模拟(Mock)或屏蔽(disable)。

单元测试带来的好处有哪些?

  • 可以检测代码是否被破坏

  • 当代码难以阅读时,阅读单元测试可以帮助我们了解其功能

  • 当系统需要重构时,单元测试可以帮助我们验证被测方法的正确性

  • 可以减少回归测试的时间成本

  • 可以使开发人员对自己的代码更有信心

单元测试相关技术?

Junit4、Junit5:单元测试运行框架

Mockito、Wiremock:mock框架,用来模拟一些对象行为

SonarQube:代码静态扫描平台,可以通过静态扫描检查代码漏洞、代码规范、代码重复率、测试覆盖率等信息

Jacoco:用来分析测试覆盖率并生成可视化报告,SonarQube通过Jacoco生成的报告进行展示。

 

作者:敲得码黛

原文链接:https://blog.csdn.net/qq_39914581/article/details/121527383

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          • 关于单元测试这个概念,我想很多前端的小伙伴都知道,但是却并不一定能描述清楚。由于我开始接触单元测试还是在四个月前,当时也只是做了一些纯函数的单元测试。所以在这里只能说浅谈一下前端单元测试。什么是单元测试?我理解的单元测试就是用于测试一个模块能否到达预期效果。通过代码来定义一个可用的衡量标准,并且可以快速检验。为什么要做单元测试?随着前端的快速发展,各类框架层出不穷,前端实现的业务逻辑也越来越复杂,这时单元测试的作用就凸显出来了。其实目前为止还是有很多代码是缺少单测的,只是现在单测的重视程度越来越高了而已。单测的好处不言而喻,首先可以提高代码的正确性,在上线前做到心里有底。其次当代码需要重构时,...
            0 0 1761
            分享
          • 什么是报表测试?最近开始在做报表测试,故名思义,就是指测试报表,报表主要是给一些特定的群体展示一些特定数据或是汇总数据,则报表测试主要是跟一堆数据打交道,检验和确认报表展示出来的数据是否正确,取值是否有误。报表测试需要做些什么?1、测试前的准备工作报表测试之前需要准备大量的数据,针对各种业务场景的数据,数据准备一定要全面。以前王豆豆没有过多地接触过报表测试,想着觉得很难,很麻烦,等到真正开始做的时候,也没有想象中的那么难,要做好报表测试,需要将前面几步做好:第一步,弄清楚业务,对于每一张报表,它反映地是什么内容;报表的含义,谁来使用这张报表,关注这张报表,关注点是什么,将这些点一一理清楚第二步...
            2 0 2763
            分享
          •   据《华尔街日报》1月7日报道,1月10日,TikTok首席执行官周受资将与欧盟委员会执行副主席、负责竞争事务的维斯塔格(Margrethe Vestager)会面。他还计划与司法专员Didier Reynders、内政专员Ylva Johansson以及负责价值和透明度的副主席Vera Jourova会面。  维斯塔格的一位发言人说,她会面的目的是评估该公司如何准备遵守今年生效的欧盟关于互联网安全和科技公司之间公平竞争的新规定。该发言人称,欧盟委员会也计划与其他科技公司会面。  在被问及即将进行的会面时,欧盟委员会的一位发言人6日表示,欧盟普遍关注TikTok以及其他应用程序的个人数据保护...
            0 0 881
            分享
          •   其实软件测试入门并不难  我们自己生活中就有接触过很多跟软件测试相关的操作。而要是从事软件测试的工作,就是需要对软件进行更加系统的测试,并把你所测试的东西进行归纳总结,对软件整个使用和运行情况做一个系统、规范的报告。  软件测试的学习大致可以分为两大类,一是:理论学习;二是:项目实操;理论部分相对实操来说会简单一些, 但理论知识是实操学习的基础,所以说想要学好测试理论和实操二者缺一不可。  软件测试风口已至  在很多人的影响中,互联网中的技术岗一直以高薪吸引着很多人想要进入这个行业里面去。  所以就有很多零基础的小伙伴一直有想要进入互联网行业的想法。但是他们都会带有些“畏惧”的成分。  因...
            0 0 1201
            分享
          • 岗位JD【技术能力】能独立完成产品线中自动化测试工作,根据测试任务,搭建软件测试环境,编写测试脚本,输出报告;【项目管理】熟练开发测试工具、测试脚本,及迭代优化测试框架,使用合理方式进行自动化管理项目;【业务推动】对测试项目的结果负责,使用合理方式推动业务端测试的效率、开发质量;【规范制定】 熟悉CI系统,完善准入/准出标准,持续提升测试效率;【效率提升】根据业务特点,引入新的测试方法和工具,探索新技术。改进测试工具或测试方法,提高效率,培训测试人员并支持技术难题解决。3年以上测试工作经验,1年以上自动化测试经验或开发经验;至少熟悉一种脚本语言,如Shell、Python、java等;至少熟悉...
            12 13 4850
            分享
      • 51testing软件测试圈微信