IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 如何正确编写单元测试? -> 正文阅读

[开发测试]如何正确编写单元测试?

如何正确编写单元测试?

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

背景

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

测试金字塔

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

文章末尾会将我学习期间产生的一些关键问题一一列出,并附上我的个人观点供大家参考与借鉴(也欢迎大家来前来找我讨论。)

单元测试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生成的报告进行展示。

尾言

单元测试固然重要,但切记:技术没有银弹!

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2021-11-26 09:08:56  更:2021-11-26 09:09:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 4:33:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码