一个初级码农的测试之旅(一)——初识单元测试

简介: 本文是一个初级程序员测试之旅的开始,简单介绍了单元测试基本概念,并用两个简单的例子说明了单元测试的重要性。

前言

首先说一下我自己——一个码农,准确的讲我是一名在中国最大互联网公司搬砖的初级码农。我不是计算机科班出身,一年前进入公司的时候,从未接触过web开发,没有完整的学习过数据库知识,写不出一条完整的sql语句,甚至不知道js和css到底是怎么控制页面行为和样式的——这样的人为什么可以通过面试?反正不是因为我长得帅。

背景知识

文章最初,先介绍一下我们团队的产品——阿里云持续交付平台(crp.aliyun.com),是一个旨在服务阿里云上众多开发者的持续交付平台(你可能还没听说过,但不妨一试哦),产品以nodejs实现,采用express框架。

接触单元测试

经历了入职最初的摸索之后,我逐渐掌握了上面提到的各种不会,开始可以用相对“丑陋”的代码来实现产品的需求,表面上看起来实现得也还不错。
为什么是表面上?因为在两次较为重要的发布之前,我们总是修复bug到凌晨之后,调试的办法当然是大家最熟悉的console.log…用过nodejs的人都知道,天生的异步会让console.log变得异常艰辛。
为什么会在上线前的人肉测试时才出bug?因为我们当时的代码是裸奔的,所谓的“赶进度”让我们时常提起单元测试的重要性却每每都在完成需求之后就去领取新的story了。
让我这个初级码农最终走上单元测试这条不归路的原因,除了上述的痛苦经历,还有一个更直接的原因——团队来了一位真正懂并且会写单元测试的哥。可以说,本文就是在他逼迫之下完成了一些个单测之后又在他“逼迫”下最终成文的。

什么是单元测试

几乎所有程序员都会说出单测的重要,但单测到底是什么,为什么重要,又应该怎么去写呢?
对于单元测试的定义,我们可以参考相对严谨的维基百科中的词条(https://en.wikipedia.org/wiki/Unit_testing) ,简单翻译就是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作,而一般的分歧则在于对最小单位划分的定义,对于这个分歧我无意深究,因为根据项目实际情况来划分会显得更加合理。

为什么选择单元测试

按照上述的定义,单元测试只能涵盖最小单位本身,并不能确保系统整体的正确性,既然如此,为什么不直接采用更全面更真实的集成测试(https://en.wikipedia.org/wiki/Integration_testing) 或者功能测试(https://en.wikipedia.org/wiki/Functional_testing) 呢?原因相信很多人都知道,这是在成本与收益之间选择了折中。成本一方面体现在复杂度,对3个模块(假设每个模块有n条路径)的单元测试,我们需要考虑n+n+n种可能,但如果是对三个模块做功能测试,那么我们则需要考虑n*n*n种可能,难易程度一目了然;成本的另一方面是时间,我们期望用5分钟甚至更少的时间去发现80%甚至更多的错误,而剩下20%左右的错误用集中的更长的时间去处理,毕竟一次代码提交之后,程序员希望尽快知道结果,以决定是修复bug还是开始新功能的开发。

两个简单的例子

毫无疑问,写单测会增加“额外”的工作量,特别是当你还不懂得如何灵活使用并驾驭测试框架的时候。但当一切都步入正轨的时候,事情就会变得美妙起来。下面我将简单的举几个nodejs的单测例子来详细说明:

示例一

某天我接了一个计算pipeline触发时间的需求,基本要求是根据触发的具体时间,换算成n秒前、n分钟前、n天前这样的显示格式(超过一年的则直接显示具体日期)。开发难度并不高(友情提示:实在不会,百度或者Google一下都有类似你想要的答案哦)。但是,我应该怎么向产品经理证明我的程序是没问题的呢?“一分钟前”、“一小时前”哪怕“一天前”都还好,但“五个月前”甚至于一年前的日期要显示具体日期,我总不能等一年才通过验收吧!这种时候,单元测试可以替我说话:
首先,我们需要准备数据:

before(function() {
    instanceTriggerInfos = [{
       instanceId = 1,
       operationTime: new Date((new Date()).getTime() - 2 * 60 * 1000)  //两分钟前
    },
    {
       instanceId = 2,
       operationTime: new Date((new Date()).getTime() - (3 * 24 + 4) * 60 * 60 * 1000)  //三天前
    },
    {
       instanceId = 3,
       operationTime: new Date('2015-1-1')  //2015年1月1日,超过一年了
    }];
});

我写了一个方法叫做formatDate,对计算结果进行判断,于是我可以写这样一个测试。

describe('function #present', function() {
    it('should only return the maximum unit', function() {
    var presentation= formatDate (instanceTriggerInfos);
        expect(presentation[instance1Id].time).to.equals(2);
        expect(presentation[instance1Id].unit).to.equals('分钟前');
    expect(presentation[instance2Id].time).to.equals(3);
       expect(presentation[instance2Id].unit).to.equals("天前");
       expect(presentation[instance3Id].time) .to.equals('2015-1-1');
   });
});

As you wish, all test pass!于是乎,我可以开开心心地交差并迎接新的挑战去了!
等等,如果需求变成了超过一年显示“1年前呢”?那我在修改代码之后,只要把最后一个断言改成如下,并通过测试是不是就可以了呢?

expect(presentation[instance3Id].time).to.equals(1);
expect(presentation[instance3Id].unit).to.equals("年前");

其实,在面对需求变更地时候,我们甚至可以改变顺序,即先修改断言,然后去修改代码,同样以代码通过测试为验收标准——有没有觉得一丝丝地耳熟,没错,这就是传说中的测试驱动开发(TDD)。

示例二

我们的产品(crp.aliyun.com,广告Again)具有定时执行任务的能力,具体实现是将定时配置丢到redis里面,然后定时去redis里面提取可以在当前周期执行的任务,并执行之。所以当用户重新配置过之后,我们需要将旧的配置删除,并创建新的配置。有一天发现在修改过之后,旧的配置并没有被删除,于是乎去追代码、找bug(没错,这段功能没有单测)——不久便找到了根源,这段代码大致如下:

var deleteSchedule = function(scheduleId) {
    var jobs = getJobs(); //获取所有job
    var targetJob = _.find(jobs, function(job) {
        return job.id === scheduleId;
    });//找到与scheduleId匹配的job
    deletetJob(targetJob);//删除targetJob
};

问题出在

job.id === scheduleId

这一判断条件,通过console.log得知传入的scheduleId是一个object:

{
    id: scheduleId
}

“万恶”的动态语言,程序不运行至此,根本不知道入参int还是object。所以我直接将判断条件改为

job.id === scheduleId.Id

并git push。此时,发生了一件忧伤的事情:
由于我们在本地给git配置了一个 pre-push的hook来自动运行单测,如果单测不通过,则拒绝push。而此次push就恰恰被拒绝了。转念一想,也是一件好事,说明我们的单测发挥作用了呀。
看了测试代码后才发现,原来deleteSchedule这个函数在另一个地方也被调用了,而那里传入的scheduleId是int,所以更加合理的修改应该是在是上述的调用处,将入参从object改为int(更符合入参名称)。这次改完之后,我没有直接push,而是老老实实的给此处调用该接口的函数加上了单测…
所以在这个例子中,单元测试不仅仅帮我发现了bug,而且还督促我把代码写的更好。

结语

作为一名立志于不断打怪升级的程序员,上面提到的测试也仅仅是测试之旅的开始,后面我会继续分享如何选择和使用单元测试框架,如何准备数据来写集成测试等等。期待与大家一起交流,让测试不再是一句口号。

目录
相关文章
|
16天前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
3月前
|
JSON Dubbo 测试技术
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
58 2
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
|
2月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
85 5
|
3月前
|
JSON 测试技术 数据格式
单元测试问题之使用JCode5插件生成测试类如何解决
单元测试问题之使用JCode5插件生成测试类如何解决
138 3
|
3月前
|
测试技术
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
48 2
|
3月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
81 0
|
3月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
59 0
|
3月前
|
测试技术 Java
全面保障Struts 2应用质量:掌握单元测试与集成测试的关键策略
【8月更文挑战第31天】Struts 2 的测试策略结合了单元测试与集成测试。单元测试聚焦于单个组件(如 Action 类)的功能验证,常用 Mockito 模拟依赖项;集成测试则关注组件间的交互,利用 Cactus 等框架确保框架拦截器和 Action 映射等按预期工作。通过确保高测试覆盖率并定期更新测试用例,可以提升应用的整体稳定性和质量。
76 0
|
3月前
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
52 0
|
3月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决