TDD:一种改变了我生活的开发方法

上午7:15 我们的技术支持充斥着工作。 早安美国公司刚刚谈到了我们,许多初次访问我们网站的人都遇到了错误。

我们真的很着急。 现在,在失去将资源访问者转变为新用户的机会之前,我们将推出修订包。 其中一位开发人员准备了一些东西。 他认为这将有助于解决该问题。 我们将尚未发布的程序的更新版本链接到公司的聊天室,并要求每个人对其进行测试。 有效!

我们的英勇工程师运行脚本来部署系统,几分钟后便开始了更新。 突然,技术支持电话的数量增加了一倍。 我们的紧急修复程序崩溃了,开发人员抓住了git的责备,工程师将系统回滚到了当时的状态。

图片

该材料的作者(我们今天出版的译文)认为,由于TDD的缘故,所有这些工作都可以避免。

为什么要使用TDD?


我已经很长时间没有这种情况了。 并不是说开发人员不再犯错误。 事实是,多年来,在我领导和影响的每个团队中,都采用了TDD方法。 当然,错误仍然会发生,但是即使从那以后软件更新的频率和在更新过程中需要解决的任务数量呈指数级增长,但可以“击倒”项目的问题的渗透率几乎已降至零。当我开始谈论的事情发生时。

当有人问我为什么他应该联系TDD时,我告诉他这个故事,我还可以回忆起其他十几个类似的案例。 我改用TDD的最重要原因之一是,这种方法提高了代码测试覆盖率,从而使生产中的错误减少40-80% 。 这是我最喜欢TDD的地方。 这从开发人员的肩膀上解决了许多问题。

另外,值得注意的是,TDD使开发人员不必担心更改代码。

在我参与的项目中,几乎每天都会进行一系列的自动模块和功能测试,这会阻止代码投入生产,这会严重破坏这些项目的工作。 例如,现在我正在查看上周进行的10次自动库更新,例如在不使用TDD的情况下发布它们之前,我担心它们会破坏某些内容。

所有这些更新都已自动集成到代码中,并且已经在生产中使用。 我没有手动检查其中的任何一个,也完全不担心它们会对项目造成不良影响。 同时,为了举这个例子,我不必花很长时间。 我刚刚打开GitHub,查看了最近的合并,然后看到了我在说什么。 以前手动解决的任务(或更糟糕的是,被忽略的问题)现在是自动化的后台过程。 您可以尝试在没有良好代码覆盖测试的情况下执行类似的操作,但是我不建议您这样做。

什么是TDD?


TDD代表测试驱动开发。 应用此方法实现的过程非常简单:


测试检测到错误,测试成功完成,执行重构

以下是使用TDD的基本原则:

  1. 在为某些功能编写实现代码之前,他们会编写一个测试,使您可以检查此将来的实现代码是否有效。 在继续下一步之前,测试已经开始并且确信它引发了错误。 因此,您可以确定测试不会产生假阳性结果,这是对测试本身的一种测试。
  2. 他们创建机会的实现,并确保该机会成功通过测试。
  3. 如有必要,执行代码重构。 在存在可以向开发人员表明系统工作正常或错误的测试的情况下进行重构,可以使开发人员对其行为有信心。

TDD如何帮助节省开发程序所需的时间?


乍看起来,编写测试似乎意味着项目代码数量的显着增加,而这一切都需要开发人员花费大量额外时间。 就我而言,起初,一切都只是这样,我试图理解,原则上如何编写可测试的代码,以及如何向已编写的代码添加测试。

TDD的特征在于一定的学习曲线,而当初学者沿着该曲线攀爬时,发展所需的时间可能会增加15-35% 。 通常,这正是发生的情况。 但是,在开始使用TDD大约两年后的某个地方,令人难以置信的事情开始发生。 就是说,例如,我开始编写单元测试时,比不使用TDD时编程要快。

几年前,我在客户端系统中实现了处理视频片段的功能。 也就是说,关键是可以允许用户指示记录片段的开头和结尾,并可以接收指向该片段的链接,从而可以引用剪辑中的特定位置而不是整个剪辑。

我没工作 玩家到达片段的结尾并继续播放,但我不知道为什么会这样。

我认为问题出在不正确地连接事件侦听器。 我的代码如下所示:

video.addEventListener('timeupdate', () => {  if (video.currentTime >= clip.stopTime) {    video.pause();  } }); 

查找问题的过程如下所示:进行更改,编译,重新启动,单击,等待...重复执行此操作序列。

为了检查引入到项目中的每个更改,花了将近一分钟的时间,而且我遇到了很多解决问题的选择(大多数选择2-3次)。

也许我在timeupdate关键字中输入了错误? 我是否了解正确使用API​​的功能? video.pause()调用video.pause()吗? 我对代码进行了更改,添加了console.log() ,返回浏览器,单击“ 按钮,单击位于所选片段末尾的位置,然后耐心等待,直到剪辑完全播放为止。 登录if结构不会导致任何结果。 它看起来像一个有关可能问题的线索。 我从API文档中复制了单词timeupdate ,以确保输入时没有timeupdate 。 我再次重新加载页面,再次单击,再次等待。 再次,该程序拒绝正常工作。

我终于将console.log()放在了if块之外。 我想:“这无济于事。” 最后, if非常简单,以至于我根本不知道如何错误地拼写它。 但是在这种情况下登录是可行的。 我cho了咖啡。 “那到底是什么!?” 我以为
墨菲的调试法则。 您从未测试过的程序的位置,因为您坚信它不能包含错误,因此将成为在完全耗尽之后才发现错误的确切位置,原因仅在于:他们已经尝试了所有可以想到的东西。

我在程序中设置了一个断点,以了解正在发生的事情。 我探讨了clip.stopTime的含义。 令我惊讶的是,它是undefined 。 怎么了 我再次查看了代码。 当用户选择片段的结束时间时,程序会将片段结束的标记放置在正确的位置,但不会设置clip.stopTime的值。 我想:“我真是个不可思议的白痴,直到我生命的尽头,才允许我进入计算机。”

多年以后,我没有忘记这一点。 一切-由于我的感觉,仍然发现了一个错误。 您可能知道我在说什么。 这一切都发生了。 而且,也许每个人都能在这个模因中认出自己。


这是我编程时的样子

如果我今天写了那个程序,我将像这样开始工作:

 describe('clipReducer/setClipStopTime', async assert => { const stopTime = 5; const clipState = {   startTime: 2,   stopTime: Infinity }; assert({   given: 'clip stop time',   should: 'set clip stop time in state',   actual: clipReducer(clipState, setClipStopTime(stopTime)),   expected: { ...clipState, stopTime } }); }); 

感觉有比这行多得多的代码:

 clip.stopTime = video.currentTime 

但这就是重点。 此代码充当规范。 这既是文档,又是代码可以按本文档要求工作的证明。 而且,由于存在此文档,如果我在片段的结束时间更改标记的工作顺序,则不必担心在引入这些更改的过程中我是否违反了剪辑结束时间的正确操作。

顺便说一下, 这里是编写单元测试的有用材料,与我们刚刚看过的相同。

关键不是输入此代码需要多长时间。 关键是如果出现问题,调试需要多长时间。 如果代码不正确,则测试将给出出色的错误报告。 我将立即知道问题不在于事件处理程序。 我将知道它是在setClipStopTime()clipReducer()中实现状态更改的。 通过测试,我将知道代码执行的功能,实际显示的内容以及期望的功能。 而且,更重要的是,我的同事将具有相同的知识,在我编写代码六个月后,他们将向其中引入新功能。

作为新项目的开始,我首先建立了一个观察者脚本 ,该脚本在每次更改某个文件时自动运行单元测试。 我经常使用两个监视器进行编程。 在其中一个上,打开了开发人员控制台,其中显示了此类脚本的结果,在另一个上,显示了我编写代码的环境的界面。 当我对代码进行更改时,通常我会在3秒钟内找出更改是否有效。

对我来说,TDD不仅仅是保险。 这是一种持续不断地实时接收我的代码状态信息的能力。 通过测试的形式获得即时奖励,或者如果我做错了事,则立即报告错误。

TDD方法学如何教我如何编写更好的代码?


我想承认一下,甚至承认这很尴尬:在了解TDD和单元测试之前,我不知道如何创建应用程序。 我完全无法想象自己是如何被录用的,但是在采访了数百名开发人员之后,我可以自信地说许多程序员处在类似的境地。 TDD方法论教会了我几乎所有有关软件组件有效分解和组成的知识(我指的是模块,功能,对象,用户界面组件等)。

这样做的原因是单元测试迫使程序员测试组件彼此之间以及与I / O子系统之间的隔离。 如果模块提供了一些输入数据,则它必须发出某些先前已知的输出数据。 如果他没有,则测试失败。 如果是,则测试成功。 这里的要点是模块应该独立于应用程序的其余部分工作。 如果您正在测试状态的逻辑,则应该能够执行此操作而无需在屏幕上显示任何内容或将任何内容保存到数据库。 如果要测试用户界面的构成,则应该能够对其进行测试,而不必将页面加载到浏览器中或访问网络资源。

除其他外,TDD方法告诉我,如果在开发用户界面组件时追求极简主义,生活会变得更加轻松。 此外,应将业务逻辑和副作用与用户界面隔离开。 从实际的角度来看,这意味着如果您使用基于组件的UI框架(例如React或Angular),建议创建负责在屏幕上显示内容的演示文稿组件和未相互连接的容器组件。混在一起。

接收某些属性的表示组件始终会产生相同的结果。 使用单元测试可以轻松地验证此类组件。 这使您可以找出组件是否可以正确地使用属性,以及在接口形成中使用的某些条件逻辑是否正确。 例如,如果列表为空,则构成列表的组件可能不显示除邀请添加新元素到列表之外的任何内容。

我早在掌握TDD之前就已经了解责任分离的原则,但是我不知道如何在不同实体之间分担责任。

单元测试使我能够研究使用mokas来测试某些东西,然后我发现嘲笑是一个标志,表明代码可能有问题 。 这让我震惊,彻底改变了我的软件编写方法。

所有软件开发都是一个组成部分:将大问题分解为许多小的,易于解决的问题,然后为构成应用程序的这些问题创建解决方案的过程。 为了进行单元测试,Tuxing表明该成分的原子单位实际上不是原子单位。 研究如何在不影响代码覆盖率的情况下摆脱麻烦,使我了解了如何确定实体之间强大联系的无数隐藏原因。

作为开发人员,这使我得以专业发展。 这教会了我如何编写更简单的代码,这些代码更易于扩展,维护和扩展。 这适用于代码本身的复杂性,以及适用于大型分布式系统(如云基础架构)中的工作组织。

TDD如何节省团队时间?


我已经说过,TDD首先可以提高测试的代码覆盖率。 这样做的原因是,在编写测试以检查此将来代码的正确操作之前,我们不会开始编写用于实现某些功能的代码。 首先我们编写一个测试。 然后,我们允许它以错误结束。 然后,我们编写实现机会的代码。 我们正在测试代码,我们收到一条错误消息,我们实现了测试的正确通过,我们执行了重构并重复了此过程。

此过程使您可以创建“栅栏”,只有很少的错误可以通过“栅栏”“跳跃”。 错误保护对整个开发团队都有惊人的影响。 这减轻了对合并团队的恐惧。

测试的高级别代码覆盖率使团队摆脱了手动控制代码库中任何甚至很小更改的愿望。 代码更改已成为工作流程的自然组成部分。

摆脱对代码进行更改的担心,就像对特定计算机的模糊处理一样。 如果不这样做,机器最终将停止运转-直到润滑并重新启动为止。

没有这种恐惧,程序的工作过程将比以前更加平静。 拉取请求不会延迟到最后一个。 CI / CD系统将运行测试,如果测试失败,它将停止对项目代码进行更改的过程。 同时,错误消息和有关错误发生的确切位置的信息将很难被发现。

这就是重点。

亲爱的读者们! 在项目上工作时会使用TDD吗?

Source: https://habr.com/ru/post/zh-CN450316/


All Articles