通过单元测试的软件质量

以下帖子基于我在2013年沙漠法令营的演讲 。 另请参阅相关的滑板

软件质量对于持续不断地向我们的用户提供新功能至关重要。 本文涵盖了软件质量的重要性以及一般如何通过单元测试,测试驱动开发和干净代码交付软件的质量。

介绍

单元测试比过去15年中遇到的任何其他技术,方法或工具提高了我的代码质量。 它可以帮助您写得更干净
代码,更快,更少的错误。 编写单元测试也感觉很好。 JUnit(或您正在使用的任何测试工具)中的绿色条带给您一种温暖的模糊感。 因此,本演讲/文章的大部分是关于单元测试的,它是更聪明的表亲TDD。

但是,除了单元测试之外,您还可以采取许多其他步骤来提高代码质量。 简单的事情如:

  • 好的变量名
  • 简短的衔接方法,一目了然
  • 避免代码气味,例如如果嵌套则长嵌套

因此,我的演讲的最后一部分将讨论一些人所说的“清洁代码”。

但是,我们首先要讨论为什么所有这些都很重要。 毕竟,设计,简洁的代码和单元测试只是达到目的的一种手段。 最终目标是: 为用户创造价值我们永远都不应忘记这一事实!

能够向项目涉众或其他开发人员解释单元测试和纯净代码的动机和优势,以及最终如何为用户带来价值是非常重要的。 因此,这就是介绍部分“软件设计的价值”的基础。

  1. 软件设计的价值
  2. 自动化测试
  3. 干净的代码

1.软件设计的价值

本部分主要基于演讲 (关键部分在45:00左右开始;另请参见论文),我很幸运地被一个名叫Martin Fowler的人参加,他是一家名为ThoughtWorks的公司的“首席科学家”。 在演讲中,Fowler很好地回答了我一直在思考的一个问题:为什么我们要关心软件中的“良好”设计?

人们可能会提出诸如以下的问题和陈述

  • 我们不需要太在意质量,因此我们可以添加更多功能
  • 我们真的需要单元测试吗?
  • 重构不会改变代码的作用,那么为什么要麻烦呢?

而且很难回答这些问题,尤其是在您承受着Swift交付的压力下。 在方法上要采取崇高的道德基础。 例如,有些人采取

  1. 错误的软件设计是一种罪过
  2. 如果您不编写单元测试,且单元测试代码覆盖率达到100%,则说明您是BAD开发人员
  3. 当您为罪恶在火热的地狱中燃烧时,名称不正确的变量或方法将烙印在您的肉上

基本上,将问题视为道德问题–如果您要设计质量低劣的软件,那么您就是一个坏人(或者至少是一个坏开发者)。

但是,我们大多数人都以谋生为生,如果我们要花时间和精力来生产高质量的软件,那么我们需要有经济上的理由来这样做-否则,何必麻烦呢? 如果我们正在编写的软件至少可以为我们的用户和利益相关者带来重大利益,那么我们可能不应该这样做。 记住我们的目标:为用户创造价值

质量甚至意味着什么?

但是在我们探讨优质软件设计的经济原因是什么之前,让我们先谈一谈质量甚至对软件意味着什么?

好吧,这可能意味着一些不同的事情,包括:

  • 优质的GUI(易于使用,外观良好,直观)
  • 少量缺陷(无错误,没有难以理解的错误消息
  • 好的模块化设计

但是,对于用户/客户/业务发起人来说,实际上只有前两个是明显的。 但是,本演讲/文章几乎只专注于最后一个-用户没有概念!

不仅是用户; 您的经理是否在晚上保持清醒状态,担心您正在生成的代码的质量? 可能不是。 我敢打赌,您经理的经理绝对不会! 您的首席执行官甚至可能都不知道什么是代码。

因此,如果管理层和用户不关心质量代码,那么为什么开发人员要关心我们呢?

福勒的设计耐力假说

马丁·福勒(Martin Fowler)很好地描述了为什么使用他所谓的设计耐力假说。

  • y轴表示您要向应用程序添加多少新功能
  • x轴代表时间
  • 橙色线代表一个假设的场景,其中您创建了一个没有设计的应用程序(并且设计中肯定包含单元测试)
  • 蓝线代表您创建具有良好设计的应用程序的场景
没有设计

Fowler的设计耐力假说基本上说,如果您编写的代码没有良好的设计,则可以从一开始就非常快速地交付代码,但是随着时间的推移,进度会越来越慢,添加新功能也变得越来越困难。当你陷入困境时

  • 在意大利面条代码中
  • 修复了您无意间破坏了一段您无法理解的代码时引入的错误
  • 花一些时间尝试理解代码,然后才能真正对其进行更改(并且仍然不太相信自己不会弄乱代码)

在最坏的情况下(上面的蓝线逐渐变细),更改将变得非常缓慢,以至于您可能会开始考虑完全重写应用程序。 因为重写整个事情,以及将花费的几个月/年的努力和血液/汗水/眼泪实际上比处理您造成的混乱更有吸引力。

那么,如何避免这种最坏的情况?我们可以收获什么经济利益?

具有良好的设计

好吧,Fowler的设计耐力假说的第二部分是好的设计如何影响累积功能。 设计,编写测试,使用TDD在短期内可能会稍长,但它的好处是,在中长期[上在该线交叉图)来看,它实际上使你更快

  • 添加新功能大约需要您花费的时间
  • 初级开发人员或新团队成员都可以在合理的时间内添加新功能

在许多情况下,这一点是几天或几周之后,而不是几个月或几年之后。

设计耐力假设摘要

在敏捷软件开发中,经常用来描述一段时间内添加的新功能数量的术语是速度。 福勒关于提高设计速度的想法只是一个假设,因为无法(轻松)证明这一点,但是对于大多数参与软件生产的人来说,它在直觉上是有意义的。

设计耐力假说:设计是赋予我们耐力的能力,它使我们能够在今天,明天以及未来数月乃至数年的时间内不断为应用程序添加新功能。

技术债务

福勒在这里所说的基本上是技术债务的概念。 技术债务是一个隐喻,指代代码库中不良设计的最终结果。 可以将债务视为需要做的实际工作之前或之后要做的额外工作。 例如,在实际添加用户请求的新功能之前必须改进设计。

在技​​术债务的隐喻下,多余的工作可以看作是利息支付。

利息支付可以采用以下形式

  1. 虫子
  2. 只需了解当前代码的功能
  3. 重构
  4. 完成未完成的工作

如何处理技术债务

当您在项目中遇到技术债务时,您基本上有两种选择:还清或接受。 偿还债务涉及花费额外的时间来清理,重构和改进设计。 好处是,由于您能够更快地添加新功能,因此最终将使您加速。 不利的一面是它现在不可避免地会使您减速。

接受债务意味着尽一切努力来添加/更改功能并继续前进。 您未来将要支付的利息是您在添加新功能之外产生的额外费用; 额外的复杂性使一切变慢和复杂。 此外,团队中的新开发人员接手也要困难得多。 最后但并非最不重要的一点是,开发人员的士气遭受了打击! 没有开发人员喜欢在无法维护的混乱代码中工作。 开发人员的营业额是非常实际的成本。

因此,当我们在设计不良的项目中遇到代码时,应该采取行动吗? 重构,添加测试,整理吗?

很长时间以来,我认为这个问题的答案就是肯定的。 总是。 但是,福勒指出,这样做并不总是在经济上明智的。

如果没有损坏,请不要修复

甚至一个模块都是一堆废话。 写得不好,没有测试,变量名不好等; 如果它

  • (令人惊讶)其中没有任何错误
  • 做到了应该做的
  • 如果您永远不需要改变

那为什么要担心呢? 从技术债务的角度来看,它没有支付太多的利息。

不要在坏的基础上建立坏的

另一方面,如果需要用功能来更新写得不好的代码,或者如果您始终发现自己陷入其中(即使只是为了理解它),那么偿还技术债务并保持代码干净,易于维护和增强

软件设计价值摘要

良好的设计,测试和良好的编码习惯等,仅是达到目的的一种手段,而该目的就是为用户提供价值。 但是,它们对于达到此目的非常有用。 它们为我们提供了持续不断地提供功能的耐力,减少了对用户的错误,因此具有非常可观的经济效益

接下来,让我们看看通过使用软件的自动化测试来实际使用良好的设计技术意味着什么……

2.自动化测试

单元测试

单元测试是一段代码,它执行代码中的特定功能(“单元”),并且

  • 确认行为或结果是否符合预期。
  • 确定代码是否“适合使用”
单元测试示例

通过示例进行解释最容易。 此示例涉及测试阶乘例程。 由n!表示的非负整数n的阶乘是所有小于或等于n的正整数的乘积。 例如,阶乘3等于6:3! = 3 x 2 x 1 = 6

我们的实现如下:公共类Math {

public int factorial(int n) {

        if (n == 1) return 1;

        return n * factorial(n-1);

    }

}

作为一名优秀的开发人员,我们添加了一个测试来确保代码符合我们的期望:

public class MathTest {

    @Test

    public void factorial_positive_integer() {

        Math math = new Math();

        int result = math.factorial(3);

        assertThat(result).isEqualTo(6);

    }

}

如果我们运行测试,我们将看到它通过。 我们的代码必须正确吗?

关于测试的一件好事是,它们使您开始考虑边缘情况。 显而易见的一是零。 因此,我们为此添加了一个测试。 在数学中,零阶阶乘为1(0!= 1),因此我们为此添加了一个检验:

public class MathTest {

…

    @Test

    public void factorial_zero() {

        Math math = new Math();

        int result = math.factorial(0);

        assertThat(result).isEqualTo(1);

    }

}

当我们运行该测试时……我们发现它失败了。 具体来说,它将导致某种堆栈溢出。 我们发现了一个错误!

问题是退出条件,或者是算法的第一行:

if (n == 1) return 1;

这需要更新以检查为零:

if (n == 0) return 1;

更新算法后,我们重新运行测试并全部通过。 秩序恢复到宇宙!

什么单元测试提供

尽管我们之前的示例演示了单元测试中发现错误的过程,但是发现错误并不是单元测试的主要好处。 相反,单元测试:

  • 驱动设计
  • 通过发现回归错误充当安全缓冲区
  • 提供文件
驱动设计

TDD可以帮助推动设计并弄清需求。 测试有效地充当了代码的第一个用户,使您考虑:

  • 该代码该做什么
  • 边界条件(0,null,-ve,太大)

他们还可以促使您使用良好的设计,例如

  • 简短的方法
    • 很难对长度为100行的方法进行单元测试,因此单元测试会迫使您编写模块化代码(低耦合,高内聚性;
  • 依赖注入

基本上,编写类与使用类不同,并且在编写代码时需要意识到这一点。

通过发现回归错误充当安全缓冲区

您是否曾经在保龄球馆里看到过缓冲器或保险杠,它们放在每个车道的边上,以供初学者或小孩使用,以阻止球跑出去? 那么单元测试就是这样。 它们通过允许重构代码而不必担心破坏现有功能来充当安全网。

对代码进行高测试覆盖率使您可以继续开发功能,而无需执行大量手动测试。 当更改引入故障时,可以快速识别并修复故障。

回归测试(检查现有功能以确保以后的代码修改不会破坏它)是单元测试的最大好处之一-尤其是当您正在开发一个大型项目而开发人员不了解ins和超出每段代码的范围,因此很可能会错误地使用其他开发人员编写的代码而引入错误。

单元测试在开发代码库时经常运行,无论是更改代码还是通过构建的自动化过程。 如果任何单元测试失败,则在更改的代码或测试本身中都将其视为错误。

文献资料

单元测试的另一个好处是,它提供了有关代码如何运行的有效文档。 单元测试用例体现了对于单元成功至关重要的特性。 测试方法名称提供了类的简要描述。

单元测试限制

单元测试当然有其局限性:

  • 无法证明没有错误

虽然单元测试可以证明存在错误,但它们永远不能证明不存在(它们可以证明不存在特定的错误,是的,但不是所有的错误)。 例如,单元测试也测试您告诉他们的内容。 如果您不考虑边缘情况,则可能不会编写任何测试功能的测试! 由于这样的原因,单元测试应该增加而不是取代手动测试。

  • 很多代码(x3-5)

在我们之前看到的简单的单元测试示例中,单元测试的代码量大约是实际测试代码的3倍–并且还有其他一些尚未测试的场景。 通常,对于编写的每一行代码,程序员通常需要3至5行测试代码。 例如,每个布尔决策语句都需要至少两个测试。 测试代码会Swift建立起来,并且编写,读取,维护和运行它们都需要花费时间。

  • 有些事情很难测试

有些事情很难测试,例如线程,GUI。

  • 测试遗留代码库可能具有挑战性

将单元测试添加到现有代码的一种常见方法是从一个包装测试开始,然后在进行过程中同时重构和添加测试。 例如,如果您有一个包含200行代码的旧方法,则可以从添加一个测试开始,对于给定的一组参数,该测试将为您提供一定的返回值。 这不会测试该方法的所有副作用(例如,调用其他对象的效果),但这只是一个起点。 然后,您可以开始将方法重构为较小的方法,同时添加单元测试。 初始的“包装器”测试将使您充满信心,即您并未从根本上破坏原始功能,而在进行重构时添加的新增量测试将使您更加自信,同时也使您能够理解(和记录) ) 编码。

值得指出的是,在某些情况下,为最初设计时未考虑单元测试的对象设置可能会带来更多麻烦。 在这种情况下,您需要做出我们前面在技术债务部分中讨论的那种决策。

因此,鉴于所有这些限制,我们应该进行单元测试吗? 绝对! 实际上,我们不仅应该进行单元测试,还应该通过测试驱动开发(TDD)让单元测试驱动开发和设计。

测试驱动开发

TDD简介

测试驱动的开发是一组鼓励简单设计和测试套件以激发信心的技术。

TDD的经典方法是红色–绿色–重构

  1. 红色-编写失败的测试,一开始可能甚至无法编译
  2. 绿色-使测试快速通过,并承担该过程中必要的所有过失
  3. 重构—消除仅使测试正常工作而产生的所有重复

红绿色/重构-TDD咒语。

没有失败的测试,就不会有新功能; 未经测试就无法重构。

TDD示例

与直接单元测试一样,通过示例可以最好地解释TDD。 但是,此部分最好视为代码屏幕截图。 请在此处查看演示幻灯片。

TDD示例中有几点值得注意:

  • 编写测试所得到的代码是干净且易于理解的; 可能比我们在事实之后添加测试(或根本没有添加)的可能性更大
  • 测试名称是很好的文档形式
  • 最后,正如我们之前预测的那样,测试代码比我们正在测试的代码多大约五倍。 这强调了为什么重构测试代码如此重要,而不是像测试的实际代码那样积极地重构它。

到目前为止,我们已经研究了为什么首先要关注良好的设计,以及如何使用自动化测试来驱动和确认我们的设计。

接下来,我们将讨论如何通过查找代码气味来发现现有代码库的问题…

3.干净的代码

确保代码干净的一种方法是避免“代码异味”。

什么是代码气味?

“代码中的某些结构暗示(有时他们为之尖叫)重构的可能性。” 马丁·福勒(Martin Fowler)。 重构:改进现有代码的设计

代码中的“气味”表示该代码可能存在问题。 引用波特兰模式存储库的Wiki,如果闻到某种气味,则肯定需要将其检出,但实际上可能不需要修复或必须容忍。 发出气味的代码本身可能需要修复,或者可能是另一个问题的症状或隐藏。 无论哪种方式,都值得研究。

我们将看看以下代码气味:

  • 重复的代码
  • 长开关/ if语句
  • 长方法
  • 方法名称不佳
  • 内嵌评论
  • 大班

重复的代码

这是#1的臭味! 它违反了DRY原理

如果您在多个地方看到相同的代码结构,那么可以肯定的是,如果找到一种统一它们的方法,则程序会更好。

症状 可能采取的行动
同一类的两个方法中的相同表达式 提取到新方法
两个兄弟子类中的相同表达式 提取到父类中的方法
两个无关类别中的相同表达 提取到新班级? 有一个类调用另一个类吗?

在所有情况下,参数化重复代码中的任何细微差别。

长开关/ if语句

这里的问题也是一个重复。

  • 相同的switch语句通常在多个位置重复。 如果向开关添加新子句,则必须找到所有这些开关,语句并进行更改。
  • 或在每个switch语句中执行类似的代码

解决的办法是经常使用多态性。 例如,如果要打开某种代码,请将逻辑移到拥有代码的类中,然后引入特定于代码的子类。 另一种方法是用国家战略设计模式。

长方法

方法越长,就越难理解。 较旧的语言在子例程调用中带来了开销。 现代的OO语言实际上消除了这种开销。

关键是好的命名。 如果您对某方法有好名声,则无需查看身体。

方法应简短(<10行)

单行方法似乎太短了,但是如果它可以使代码更清晰,那么即使这样做也可以。

积极分解方法!

将方法分解为较短方法的真正关键是要避免使用较差的方法名称…

方法名称不佳

方法名称应具有描述性,例如

  • int process(int id){//不好!
  • int computeAccountBalance(int accountID){//更好

也就是说,方法名称应说明该方法在无需阅读代码的情况下所做的事情,或者至少,快速扫描该代码应确认该方法是否符合其要求。

内嵌评论

是的,在线注释可以被视为代码异味! 如果代码很难遵循,您需要添加注释来描述它,请考虑重构!

最好的“注释”就是您为您提供的方法和变量的名称。

但是请注意,Javadocs(特别是关于公共方法的)很好。

大班

当一个班级尝试做太多事情时,它通常显示为:

  • 方法太多(> 10个公共方法?)
  • 代码太多
  • 实例变量太多–每个方法都使用每个实例变量吗?

解决方案

  • 消除冗余/重复代码
  • 提取新/子类

干净的代码摘要

最重要的一件事就是弄清楚代码的意图。

您的代码应该清晰,简洁且易于理解,因为尽管一行代码只编写了一次,但很可能会被多次读取。

一两个月后,您能否理解您的意图? 请问你的同事? 请问初级开发人员?

一个软件程序在其整个生命周期内平均将拥有10代维护程序员。

维护此类不可读的代码是一项艰苦的工作,因为我们会花费大量精力来了解我们正在查看的内容。 但是,不仅如此。 研究表明,不良的可读性与缺陷密度密切相关。 难以阅读的代码也往往很难测试,从而导致编写的测试更少。

摘要

  • 良好的设计使我们具有持续不断地交付业务价值的毅力
  • 单元测试是良好设计不可或缺的一部分; TDD更好
  • 好的设计也可以只是更简洁的代码。 积极重构以实现这一目标

最后的想法:每次编写代码时,都要做一个小的改进!

参考: Shaun Abram博客博客中的JCG合作伙伴 Shaun Abram提供的通过单元测试的软件质量

翻译自: https://www.javacodegeeks.com/2013/06/software-quality-via-unit-testing.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值