测试不仅仅是为了发现错误,它们是一种持续的对话,揭示隐藏的设计缺陷、客户偏差以及预见未来的问题。
大家通常认为测试是代码开发与发布之间最后的一道质量防线。大多数人仅仅关心用例的执行通过/失败验证(报告),而不是深入挖掘报告背后为我们提供的关于设计质量、逻辑准确性和实现稳健性的深刻见解。
软件测试,尤其是融入敏捷方法论和测试驱动开发实践的测试,不能仅仅把它看作部署前的检查点。测试是一种持续的对话,一个不断循环的反。如果深入分析,可以揭示出比缺陷存在的更多信息。它们可以指出设计缺陷、与客户需求不符的问题,甚至预见尚未在项目中出现的问题。
然而,这些丰富的信息只对那些愿意“倾听”的人开放。作为开发人员,需要培养解读测试试图传达的信息的能力,能够从结果的字里行间中读出含义,并且要明白,编写困难的测试、创建复杂的模拟mock或者测试结果时而成功时而失败,这些都不是单纯的技术障碍;它们仅仅是外在的表象,如果能够正确理解其内在含义,就可以引导我们编写出更干净的代码、更稳定的设计,并实现真正符合客户期望的交付产品。
本文旨在探讨开发者如何提升“聆听”测试的能力。我们将深入探讨测试的难度如何反映设计中的不必要复杂性。
倾听背后的深意
冰山原理大家都知道吧,冰山只有一小部分冰露在水面上,而大部分隐藏在水下。这教会大家一个道理:我们看到的表面现象可能只是更大现实的一小部分。同样,在软件工程中,我们观察到的“结果”——只是我们对代码和测试结果的初步分析——但它可能会欺骗我们,让我们以为一切都井然有序,而实际上,更大的问题隐藏在表面之下,等待浮现出来。
冰山原理同样适用于我们在软件开发中对测试的“倾听”。正如只有冰山的顶端可见一样,对测试结果的初步浅层分析似乎已经足够。然而,正如我们需要深入冰山表面以下以了解其真正的规模一样,我们必须“深入”测试以揭示可能隐藏的深层次问题。
肤浅的分析可能会得出错误的结论,但对测试失败的详细挖掘可以揭示隐藏的问题,比如冰层下的裂缝,否则这些问题将无法被发现。这种关注尤其重要,因为无论是单元测试、集成测试、契约测试还是端到端测试,它们传递的信息不仅仅是用例执行通过/失败的结果。
它们会提醒我们系统所基于的假设、系统应达到的预期以及实际结果与预期之间的差异,这些会揭示整个冰山的结构,展示系统在理想条件下应如何运行,但也会暴露出可能出现故障的边界和限制条件。如果我们不关注这些,就可能会忽略一些重要的警示信息,这些信息可能会导致系统在实际运行时出现灾难性的故障。
当我们考虑系统设计和业务逻辑时,测试就像一面镜子,不仅反映了代码的当前状态,也反映了我们在开发过程中所做的设计决策和假设。如果一个测试变得过于复杂或难以编写,这并不是测试本身的失败,而是技术方案设计的失败。
这种过度复杂可能是由各种不当的做法导致的,比如组件之间的耦合度很高、职责划分不够明确或者业务逻辑深深地嵌入了本应与之无关的层中。
同样,未能捕捉和防止常见错误条件(如空指针或未定义变量)的测试,暴露了我们在预见性和规划方面的不足。作为程序员,我们可能会倾向于只关注“快乐路径”,而忽视可能的失败状态。测试是我们的安全网,当它们无法保护我们时,往往是因为我们没有正确地为它们提供“装备”。
在编写单元测试和端到端测试时,程序员常犯的一些错误包括:
1. 覆盖不足:未能考虑并测试所有可能的场景,尤其是边缘情况和错误条件。
2. 过度耦合:编写的测试过于依赖内部实现细节,使得它们在重构时变得脆弱。
3. 非确定性测试:创建依赖于全局状态、时间或外部数据的测试,这些测试可能无法预测地通过或失败。
4. 需求不明确:编写的测试没有清楚地传达它们所验证的需求,这使得它们难以维护和理解。
5. 表面测试:过度依赖仅检查代码最表面特性的测试,而没有深入探究底层逻辑和业务规则。
在端到端测试的背景下,常见的错误包括:
1. 不一致的环境:未能确保测试环境与生产环境高度相似,从而导致假阳性或假阴性结果。
2. 不当的被模拟依赖项:未能将正在测试的组件与其外部依赖项隔离开来,导致测试更关注集成而非特定功能。
3. 测试数据不足:使用的测试数据集不足以涵盖现实场景的多样性。
此外,对于过度敏感于重构的测试尤其存在问题。它们会破坏对测试套件的信心,进而破坏对项目本身的信心。当简单的代码改进或性能优化需要对测试进行大规模审查时,这不仅会阻碍健康的重构,还会表明测试过于纠缠于实现细节,而不是关注期望的行为和结果。
当事情变得很明显出了问题…
想象一下自己坐在电脑前,看着自己刚刚编写的代码。你知道测试很重要;它们是你的安全防线,确保任何未来的更改都不会破坏现有的功能。然而,当你开始编写测试时,你遇到了一系列障碍。突然之间,你发现自己陷入了一片mock的海洋中,只是为了测试一个单一的功能。这种经历听起来是否很熟悉?
这是代码设计时没有考虑可测试性的典型问题。匆忙的设计和架构决策会导致类之间的耦合度很高,使每个组件都依赖于其他组件的内部细节。这种相互依赖是编写测试时产生许多问题的根源。
耦合与职责分离
单一职责原则是软件工程中的基本原则。它要求将应用程序划分为不同的部分,每个部分专注于解决特定的问题。当类或模块紧密耦合时,系统中一个部分的更改可能会触发其他部分(包括测试)的一系列调整。
这为什么会成为问题呢?
1. 难以隔离出可进行测试的功能:如果为了测试一个简单的功能而需要实例化系统的一半,那么就存在问题了。每个测试都应该能够专注于特定的行为,而不必担心系统的其他部分。
2. 过度的模拟:当你发现自己为一个类的依赖关系创建了一个又一个模拟对象时,这意味着这个类承担了太多的工作或者与其他系统功能过于紧密耦合。
3. 测试脆弱性:与被测试代码高度耦合的测试是脆弱的。即使功能保持不变,但只要实现稍有变化,就可能导致多个测试失效。
考虑以下这些假设的场景,其中代码设计限制了测试的进行。
1. 购物车:计算税费、折扣、完成支付
场景:一个计算税费、折扣并支持支付的购物车类:要测试这些功能,需要一个装有商品的购物车实例、一个税费系统以及可能的外部支付处理服务。当尝试模拟故障时,问题就出现了。如果支付服务不可用,你如何测试购物车的行为?恐怕不得不创建一个支付服务的模拟Mock,另一个税费系统的模拟Mock等等。
这里的问题不仅仅是需要使用mock对象,而是缺乏职责分离。如果一个类负责计算税费、折扣和支付等多个职责,我们就会遇到将多个职责耦合在一起的代码。这会使代码更不模块化,更难测试,因为要验证某个功能,你不得不mock系统中的多个部分。
当我们试图将每个功能隔离开来时,复杂度会以指数级增加,从而降低了可测试性。一种解决方案是应用单一职责原则,将这些职责划分为独立的类(例如, TaxCalculator 、 DiscountApplier 、 PurchaseFinalizer )。这将允许独立地测试每个组件,并减少对无关组件的模拟需求。
2. 用户管理器:创建用户并发送电子邮件
场景:既能管理用户又能发送确认邮件的用户管理器:测试创建用户可能很简单,但如果我们想要测试发送邮件呢?你需要模拟电子邮件服务。如果发送邮件失败了呢?在不发送真实邮件的情况下,如何测试系统在出现此类失败时的行为是否正确?
在这里,主要问题是业务逻辑与基础设施逻辑(发送电子邮件)的混合。当用户管理器同时负责这两者时,可测试性就会受到影响,因为任何用户创建测试现在都依赖于外部电子邮件服务。
为了提高可测试性,我们应该将发送电子邮件的逻辑分离到一个单独的组件或服务(EmailService)中。这样,我们就可以在不考虑发送电子邮件的情况下独立测试用户创建,反之亦然。在测试期间,可以使用依赖注入提供一个EmailService的模拟版本,这样我们就可以在不涉及真实电子邮件服务的情况下测试系统在发送电子邮件时如何处理失败。
3. 报告系统:数据提取、转换和呈现
场景:一个提取、转换和呈现数据的报告系统:想象一下,该系统从多个来源提取数据,对其进行转换,然后生成报告。要仅测试报告生成功能,就需要模拟数据提取和转换操作,如果这些操作复杂,那么这将是一项艰巨的任务。
这种情况凸显了过程步骤缺乏模块化和适当的封装问题。由于提取、转换和呈现紧密耦合,因此测试报告生成变得困难。这导致了复杂的依赖关系,使得过程的每个部分都需要被模拟,以便测试其他部分。
为了提高可测试性,必须将过程分解为独立的、定义明确的组件:DataExtractor 、 DataTransformer 和 ReportGenerator 。这些组件中的每一个都可以独立进行测试。此外,通过模拟这些步骤的接口,您可以无需担心提取和转换操作的复杂性,而只需专注于特定的报告生成逻辑即可测试ReportGenerator。
在这些例子中,由于缺乏适当的职责分离,导致测试特定行为时需要进行大量的设置和模拟,这不仅使得测试更难编写和维护,而且因为它们与现实世界使用场景的距离更远,因此也降低了测试的可靠性。
但是我们错过了什么呢?
当你发现自己陷入了测试的复杂性中,仿佛在迷宫中穿行时,不要灰心丧气。这是一个重要的信号——隧道尽头的一束光。你的测试正在告诉你一些至关重要的事情:它们突出了你代码设计中的问题,而这些问题可能并不显而易见。这是一个关键时刻,是一个调整代码的良机。
Kent Beck是极限编程和测试驱动开发(TDD)的先驱之一,他经常强调倾听你的测试试图传达的信息的重要性。当编写测试成为一种负担,当每个测试似乎都需要不相称的设置和配置时,这显然是一个信号,表明您的设计可能存在问题,或者您专注于测试可能不应被测试的内容。Beck鼓励我们将每个测试遇到的困难视为指导,而不是障碍,指向我们代码中可以改进的区域。
马丁·福勒(Martin Fowler)是软件设计和重构领域的另一位巨匠,他也谈到了这个问题,强调代码应该设计得便于进行测试。如果情况并非如此,如果测试变得复杂且容易出错,这表明代码的设计可能违反了良好的设计原则,比如职责分离和低耦合。
然而,问题在于,许多开发者可能由于时间压力或缺乏经验,倾向于忽视这些信号。他们没有把测试难度看作有价值的反馈,而是把它当作一种干扰,更糟的是,把它当作完全跳过测试的借口,这是一个严重的错误。忽视测试反馈就像忽视汽车仪表盘上的警告灯。你可能还能继续开车一段时间,但最终,被忽视的问题可能会以灾难性的方式显现出来。
现实情况是,测试提供了一个独特的机会,不仅可以验证功能的行为,还可以验证代码设计的质量。当编写测试变得困难时,当您必须穿过多个层级的模拟层时,或者当代码中的小改动导致多个测试失败时,这些都是您的系统设计可能需要进行根本性调整的迹象。
但是我们必须时刻提防逻辑谬误。让我更详细地解释一下我的意思。
关于单元测试的重大谬误
我们关于测试和代码设计的对话现在扩展到了一个我必须讨论的有争议的话题:围绕单元测试的谬论。在我们的职业生涯中,我们许多人都学习了测试代码的重要性。然而,一种错误的信念经常潜入我们的开发实践中,即只要我们的单元测试通过,我们的代码就是可靠的,其设计也是合理的。有些人认为,如果所有的单元测试都是“绿色”的,那么我们的代码就没有问题。这种心态可能会产生误导和危险。
虚假的安全感
首先,单元测试至关重要。它们是代码小单元预期行为的守护者,确保每个函数或方法都能正常工作。然而,认为通过单元测试就等于拥有一个完全健康的系统是一种过于简单的看法。这种信念会带来一种虚假的安全感。
把单元测试看作是对一本书中每个单词的检查。每个单词都可能是正确的,但这并不能保证句子有意义、段落连贯,或者整个故事引人入胜且没有矛盾。
在现代系统中,尤其是那些采用微服务等复杂架构构建的系统,真正的健壮性是在集成和整个系统的行为中得到检验的。单元测试可以验证每个服务在孤立状态下是否正常工作,但当它们开始相互交互时会发生什么情况呢?如果这些服务之间的通信失败,或者共享数据的格式与预期不符又会怎样呢?
单元测试的谬误忽视了这些复杂性。它忽视了这样一个事实:真正的挑战往往存在于不同服务单元之间的微妙交互之中,而不仅仅是每个单元内部。
此外,单元测试的谬误可能会掩盖设计问题。一个代码库可能拥有100%的测试覆盖率,但仍然可能存在诸如高耦合、低内聚或违反单一职责原则等糟糕的实践。这些问题不一定会被单元测试捕获,但会对系统的可维护性和可扩展性产生重大影响。
行为与实现
另一个值得注意的方面是行为测试与实现测试之间的差异。许多单元测试最终都集中在代码如何执行任务上,而不是该任务的实际内容。这会导致这样的情况:即使代码的外部行为保持不变,但只要实现上有微小的改动,就会导致测试失败。这是一个关键的区别,因为良好的测试应该允许代码在不失败的情况下进行重构,只要保持所需的行为即可。
单元测试无疑是必不可少的,但这并不意味着我们应该只依赖它们或者满足于编写基本的用例。单元测试所做的不仅仅是一些人可能认为的那样——它们揭示了我们的代码是否易于维护。
“覆盖的幻觉”
这种谬误的一种表现形式是对测试覆盖率的痴迷。虽然高代码覆盖率可能看起来令人安心,但它并不能保证系统中的关键行为得到了适当的验证。代码覆盖率告诉我们,在测试期间某些代码被执行了,但这并不一定意味着测试验证了正确的预期。完全有可能存在代码覆盖率为100%但依然存在大量缺陷的系统。
过度强调实现细节
单元测试的另一个陷阱是过分强调实现细节。与所测试代码紧密耦合的测试往往很脆弱,需要不断维护。此外,它们可能会使我们忽视更大的画面,导致我们忽视整个系统的行为。当测试由于没有影响系统可观察行为的更改而失败时,它们就失去了价值,成为障碍。
复杂的行为需要一个策略
当我们谈论由多个相互连接的组件或服务组成的复杂系统时,会出现一个有趣的现象:涌现行为。这些行为无法通过单独观察系统中的每个部分来预测。只有当所有部分开始相互作用时,这些行为才会显现出来,就像一个管弦乐队,只有所有乐器共同贡献才能产生和谐的效果。
单元测试面临的挑战在于:它们就像显微镜,非常适合详细检查交响乐队中每件乐器的情况,确保每件乐器都调准并处于最佳状态。然而,尽管验证这一点非常重要,但它并不能告诉我们一切。单元测试可以告诉你小提琴是否调准了,但无法告诉你它与大提琴、长笛和小号的合奏效果如何。
因此,尽管单元测试对于确保每个单独组件的质量至关重要,但它们无法揭示这些组件在组合在一起时的行为。正是在这些交互点上,在相互作用中,可能会出现意想不到的行为——这些行为并非故意编写的,而是由系统各部分之间复杂的关系网络所产生的。这些问题通常是难以追踪的复杂错误的根源,因为它们既未被预期,也无法从单独分析组件中轻易预测。
为了捕捉和理解这些涌现行为,我们需要一种更全面的测试方法,而不仅仅是单元测试。我们需要测试整个系统,考虑所有组件之间的交互和流向。只有这样,我们才能全面了解系统,包括只有在所有组件协同工作时才会出现的意想不到的行为。
因此,最大的谬误就是认为通过单元测试就等于拥有设计良好且功能完备的系统。这种狭隘的观点可能会使我们偏离测试的真正目的,即确保代码在最细粒度层面上能够按照预期执行,同时也确保整个系统在所有情况下都能正确且可预测地运行。
单元测试
目前我认为单元测试是微服务开发过程中的一种测试策略,用于验证业务逻辑并检测可能导致重大问题的回归问题。它是测试金字塔的基础,但我们还可以采用其他策略。
然而,当我们专注于微服务之间的协调时,我们意识到单元测试的局限性。它们能够确保每个组件在孤立状态下正常工作,但无法告诉我们整个系统是否以集成、协调的方式运行。当我们需要验证服务之间的交互、确保API合同得到遵守或验证消息是否正确通过事件总线传输时,单元测试就像是需要完美和谐的交响乐中单个的乐器。
这就是集成测试、合同测试和端到端测试发挥作用的地方。每种类型的测试都从不同的角度来检查系统,确保组件不仅在单独运行时功能良好,而且在作为更大集合的一部分时也功能良好。
集成测试
例如,集成测试就像外交官,确保服务之间能够相互理解,交易能够在服务边界之间顺畅流动。它们对于发现误解和沟通障碍所在至关重要,这样可以在小错误升级为重大问题之前进行调整。
契约测试的重要性
另一方面,契约测试就像法律协议一样,确保每个服务都能理解并尊重对方的期望。它们是保证,即使服务被更新或修改,它仍然能够履行对依赖服务的义务,防止意外的连锁失败。
端到端测试的观点
最后,端到端测试是全方位的视角,可以全面检查整个系统,确保从开始到结束的整个业务流程都能按预期运行。这些测试验证了用户旅程,从第一次点击到最终结果,确保用户体验与设计时一样流畅和有效。
认识到测试如何能够揭示一个类所承担的责任是非常重要的。当我们遇到需要实例化大量依赖项或设置大量模拟对象来测试一个类的情况时,这显然是一个明显的信号,表明这个类可能承担了太多的责任。这些情况表明,单一职责原则可能被忽视了。
过度依赖
当一个类具有许多外部依赖时,通常意味着它承担了超出其职责范围的工作。例如,一个负责业务逻辑、数据持久化和网络通信的类显然承担了太多的责任。这些责任的累积不仅使得类难以测试,而且难以理解和维护。理想情况下,每个类都应该只承担单一职责,专注于系统功能的某个特定领域。
重载方法
同样,看起来承担了太多任务的方法可能是这些职责应该在多个类或函数之间更合理地分配的标志。如果一个方法过于复杂,以至于测试它需要一系列的条件和场景,那么可能该考虑是否可以将该方法分解为更小的部分,每个部分都封装了更易于单独测试的特定功能。
忽视反馈
回到测试提供反馈这一点,重要的是要认识到编写或维护测试的困难本身就是一种有价值的反馈。当我们在尝试测试某个行为时遇到阻力时,这通常意味着我们代码的设计存在问题。忽视这种反馈——无论是由于疏忽、缺乏理解还是“只想让它工作”的愿望——都是错过了提高系统质量和可维护性的机会。
另一个相关的观察是,当一个类中的某些条件或错误场景特别难以测试时。这可能意味着该类正在处理太多的情况,其中一些情况可能更适合由系统中的其他部分来处理。某些执行路径难以测试通常表明该类存在不必要的复杂性或职责不清晰的问题。
沉默的测试:隐藏在字里行间的危险
除了关注正在测试的内容外,同样重要的是要意识到测试的沉默——它们没有告诉我们的内容。通常,我们可能会被这种沉默误导,以为一切都很顺利,而实际上一些关键的问题可能被忽视了。想象一下,你系统中的一个重要行为已经退化了,或者更糟糕的是,一个应该在某些条件下失败的关键功能现在被测试所忽视。这种沉默可能比失败的测试更危险,因为它会制造一种虚假的安全感,让真正的问题隐藏在表面之下。
当测试未能向我们发出这些回归或意料之外行为的警报时,我们必须问自己:为什么会发生这种情况?代码中有没有某个区域的可测试性受到了损害?可能是管理不当的依赖关系或未得到适当隔离的复杂性问题?此时,我们需要仔细聆听测试的沉默(或更确切地说,是它未能告诉我们的内容)。
在开发领域,这种问题可能会以多种方式出现。罗伯特·马丁(Uncle Bob)在他的《Clean Code》一书中将这种问题称为“上帝类”。“上帝类”是指一个承担了太多职责的类,在系统中扮演着真正的“上帝”角色,难以测试和维护。当一个类承担了过多的责任时,系统行为中一些关键部分往往会被遗漏在测试之外。这不仅是设计不佳的标志,也是可能导致沉默型错误(未被注意到的错误)的潜在来源。
同样,马丁·福勒(Martin Fowler)在《重构:改善现有代码的设计》一书中强调了重构和简化设计的重要性,以提高代码的可测试性。福勒认为,当代码过于复杂,难以进行有效测试时,这显然表明需要重新评估某些东西。糟糕代码的“气味”通常表现为难以编写适当的测试,而正是在这些时刻,测试的沉默可能会变得令人难以忍受。
我们来将这一讨论应用到“优惠券微服务”的场景中。优惠券系统看似简单,但其中包含了许多业务规则,决定了优惠券的使用条件、时间和对象。这些规则对于确保系统正常运行至关重要,如果我们没有充分测试它们,最终可能会出现一些不易察觉的严重问题。
优惠券验证:错误之前的沉默
想象一下,系统在将优惠券应用于购买之前需要进行验证。规则很简单:“如果优惠券的有效期尚未过期,则该优惠券有效。” 现在,想象一下有人更改了代码以调整日期的处理方式,但由于疏忽,应该确保基于不同时区的优惠券有效性的测试被删除或静默了。这个测试曾经是保护系统完整性的沉默守护者,但现在却一言不发。系统可能会开始拒绝那些本应有效的优惠券,直到客户开始抱怨时,才没有人会注意到这个问题。
折扣申请:未显现的失败
另一个例子是折扣应用。假设代码被更改以允许使用新的折扣类型,但测试没有更新以涵盖所有场景,尤其是不应适用折扣的情况。如果未对正确行为进行测试,或者关键测试被意外静默,我们最终可能会出现折扣错误的情况,导致资金损失。
优惠券兼容性:沉默的代价
再次考虑一下优惠券与其他促销活动的兼容性。每张优惠券都有自己的兼容性规则,这些规则需要经过严格的测试。如果由于执行不当的重构或代码调整而使验证兼容性规则的测试失效,那么系统可能会开始允许使用原本不被允许的折扣组合。测试中的沉默看似无害,但可能导致真正的损害,比如客户投诉甚至收入损失。
持续的维护与观察
检测并避免测试中的沉默现象需要结合编写专注于可观察行为的测试以及持续不断地对这些测试进行重审和更新,以适应系统不断演进的需要。这是一个持续的观察循环,其中测试不仅仅是验证代码是否工作的工具,也是反映系统在所有可能情况下应如何行为的反映。
因此,在开发系统或微服务时,要专注于可观察的行为,并定期重审您的测试。这不仅可以防止测试沉默,还可以确保您在长期内保持可靠、高质量的软件。
沉默其实是一种求救的呼喊!
当我们避免测试程序的某些部分时,就好像忽视了汽车发动机的异响。起初可能看起来不是什么大问题,但它可能是更严重问题的征兆。在软件开发中忽视这些信号不仅会导致漏洞和系统故障,还会让代码维护变成一场真正的噩梦。
现在来说说维护问题。一个业务规则未经充分测试的系统就像一辆从未保养过的汽车。每次进行更改或更新时,你都屏住呼吸,祈祷一切顺利。如果出了问题呢?如果突然间系统开始批准所有借款人的贷款申请,而不管他们的还款能力呢?在问题影响到用户之后再去解决这些问题所带来的成本将是巨大的,无论是从财务上还是从声誉上。
在这种情况下,测试超越了其基本的代码验证功能,成为真正帮助确认软件是否能够有效执行业务关键任务的伙伴。让我们更深入地探讨一下。
开发者的角色
对于你(开发者)来说,沉浸在这个细致的流程中时,要记住你编写的每一个测试都是软件功能这个更大拼图的一部分。你的工作不仅仅是编写代码,而是将商业愿景变为现实,确保软件不仅在技术层面上“运行”,而且能够真正成为实现业务目标的助力。当然,我们并不孤单,我们可以使用一些技术和策略来帮助我们,比如行为驱动开发(BDD)。
行为驱动开发(BDD)
想象一下这样的场景:你的开发团队正面临着一个具有挑战性的局面:复杂的业务规则、紧迫的截止期限,以及需要不断将技术工作与客户期望保持一致的情况。你有多少次在技术会议上发现,尽管分享了大量知识,但似乎还是缺少了什么?比如一个关于我们为什么要构建这个东西的清晰愿景,而不仅仅是如何让它运行。
这就是行为驱动开发(Behavior-Driven Development)的闪光之处。它不仅仅是一种测试技术,而是一种将软件预期行为置于一切中心的哲学。但在实践中它是如何工作的,它如何真正帮助团队节省时间并构建有意义的软件呢?
不仅仅是考试
BDD不仅仅是一种编写测试的不同方式。它是一种方法,在编写代码之前就开始使用。BDD鼓励我们探索为什么要构建某物,并与所有利益相关者合作,确定软件的预期行为。在这里,“3 Amigos”规则发挥作用——开发人员、测试人员和业务代表共同定义系统的行为。
这种初步合作至关重要。通过讨论预期的行为,团队中的每个人都会对最终目的有一个清晰的认识,从而使整个开发过程中的决策更加容易。正如《BDD实战》一书中约翰·弗格森·斯马特所说:“BDD不仅仅是关于如何编写好的测试——它是关于如何编写正确的软件。” 想想看:如果每个人都从一开始就知道一个功能的真正目标,可以节省多少时间?
3 Amigos 法则:合作创造价值
“3 Amigos 法则”是一种体现了 BDD 精髓的做法。让开发人员、测试人员和业务代表聚在一起意味着什么?这意味着在编写任何代码之前,这三个视角就已就需要完成的工作达成一致。这不仅可以减少误解的风险,还可以加快开发进程,因为预期是明确且定义良好的。
让我们举一个实际的例子。开发人员通常认为“贷款审批”只需要正常运行即可,但与“3 Amigos”的对话可能会揭示一些细微之处,比如“批准贷款时应评估哪些具体标准?”或者“我们应该如何计算利率以符合监管要求?”这些问题有助于以一种能准确捕捉软件成功所需要的条件的方式来制定测试用例,而不仅仅是从技术角度,还包括满足业务需求的角度。
BDD深度剖析:聆听你的测试
现在,想象一下你正在基于BDD定义的场景编写测试用例。每个测试用例都反映了一次对话,一次对业务真正关注点的发现。这完全改变了代码开发和测试的方式。当BDD被良好地实施时,它可以确保代码不仅通过了测试,而且这些测试与所有相关方的期望相一致。
这种做法可以节省时间。想想如果花费时间开发出一个最终未能达到预期目标的功能,会浪费多少时间。通过BDD,你可以避免这些问题,因为从一开始就由整个团队讨论和验证的用例指导着开发过程的每个步骤。
约翰·费格森·斯马特在《BDD实践》一书中强调:“BDD的美妙之处在于测试的简单和清晰——它们是对所需行为的一种表达,而不是孤立的技术检查。”换句话说,通过聆听你的测试(字面意思),观察它们对系统行为的描述,你实际上是在倾听业务需求。这将使测试成为一种战略工具,而不仅仅是需要克服的技术障碍。
将BDD付诸实践:通往成功的道路
既然我们已经认识到BDD的价值,那么如何有效地将其应用于实践呢?以下是一些步骤:“既然我们已经认识到BDD的价值,那么如何有效地将其应用于实践呢?以下是一些步骤:”
遵循“三剑客法则”:每当提出一个新的功能时,召集“三剑客”(开发人员、测试人员和业务代表)一起工作。共同定义预期的行为和验收标准。
使用自然语言编写测试用例:使用Cucumber或JBehave等工具编写易于理解的测试用例,这不仅有助于沟通,而且确保代码由清晰且定义明确的行为驱动。
保持对行为的关注,而不是实现方式:编写测试时,应关注系统应该做什么,而不是如何实现。这样可以为技术实现提供灵活性,同时保持与业务需求的一致性。
不断回顾与完善:BDD 是一个迭代的过程。随着项目的推进,重新审视测试用例,与“3 Amigos”团队进行讨论,并完善行为以确保开发工作继续与业务目标保持一致。
在适当的时候提出恰当的问题
在开发新功能时,你或你的团队是否曾多次怀疑真正需要交付的内容?也许是因为文档不够清晰,也可能是因为需求发生了变化,但没有及时通知所有人。不幸的是,这种情况比我们愿意承认的更为常见,往往会导致需要重新开发或调整的开发工作,浪费宝贵的时间和资源。行为驱动开发(BDD)通过指导团队在适当的时间提出正确的问题,为解决这一问题提供了一种方法。
开发初期问题的重要性
在软件开发中,最具影响力的时刻是开发过程的开始。在这个阶段做出的决定可以决定项目的成败。这就是BDD大放异彩的地方,它鼓励在编写任何代码之前就提出基本问题。
BDD提倡一种以问题为中心的方法,旨在调查系统预期的行为。与“我们将如何实现此功能”不同,BDD引导我们问“为什么需要此功能”和“为了满足业务期望,该功能需要做些什么”?这些问题对于确保团队中的每个人都理解该功能的真实目的至关重要。
在《行为驱动开发实战》一书中,约翰·费格森提到:“当你从行为开始时,技术就会成为次要考虑的因素。当你理解了需要实现的目标后,自然就知道该如何实现它了。”这意味着,通过从一开始就关注“为什么”和“是什么”,团队就能更好地做出技术决策,以满足业务的实际需求。
常见错误:在没有充分理解的情况下匆忙编写代码
许多工程团队没有投入足够的时间来解决这些最初的问题。急于交付的压力、认为尽快开始编写代码是最高效路径的错误感觉,以及利益相关者之间缺乏明确的沟通都是导致这一问题的原因。
结果呢?技术上正确但未能解决正确问题的功能。或者更糟糕的是,因为在开发过程中出现了新的信息,导致需要重新设计功能——如果在一开始就问对了问题,这些信息本来是可以被识别出来的。
一个典型的例子是,一个团队在开发一个电子商务系统的新支付功能时,没有完全理解不同支付方式背后的业务规则。从技术角度来看,他们有效地实现了该功能,但当系统上线时,却发现它不符合某些国家信用卡交易的监管要求。这种失败并不是因为缺乏技术能力,而是因为在开发开始之前,应该询问和回答的问题不够明确和一致。
BDD是如何促进提出正确的问题?
BDD通过构建测试用例来组织开发过程,确保提出这些关键问题。这些测试用例以自然语言编写,并由所有相关人员(即我们之前提到的“3 Amigos”)讨论和验证。
在编写BDD用例时,每个用例都应反映出软件需要处理的实际场景。例如,在贷款系统中,用例不应仅限于“如果信用评分足够高,系统应批准贷款”。它们应该更进一步,提出问题:“如果客户信用评分足够高,但存在需要考虑的现有债务时会发生什么?”或者“如果在贷款申请过程中在验证客户数据时出现错误,系统应如何反应?”
这些问题并非微不足道。它们迫使团队考虑所有可能影响系统行为的因素,从而更深入地了解需要构建的内容。此外,通过在测试用例中记录这些问题,它们被记录下来并为团队中的每个人可见,这有助于在整个项目中促进沟通和协调。
工程团队为何会失败
尽管显而易见的好处,但许多工程团队仍然无法有效地实施BDD。一个常见的错误是将BDD仅仅看作是又一层自动化测试,而没有抓住机会去探索和文档化系统的预期行为。
另一个问题是缺乏所有利益相关者的参与。只有在开发人员、测试人员和业务代表之间进行协作时,BDD才能发挥作用。如果其中任何一个群体没有参与定义行为场景,那么该过程的有效性就会大大降低。
有些团队低估了投入到这些初步讨论中的时间的重要性。在以截止日期为导向的环境中,“我们没有时间参加这么多会议”的想法似乎是合理的,但事实是,在项目生命周期中投入的时间会被多次收回。当在适当的时间问出正确的问题时,误解就会减少,重新工作的需求也会大大降低。BDD引导团队在适当的时间问出正确的问题,确保开发的内容真正满足业务需求。
因此,下次你的团队准备开始一个新的功能时,请停下来思考一下:我们真的理解我们需要构建什么吗?我们问对问题了吗?如果答案不明确,那么可能是时候重新评估一下流程,让BDD引导你进行更有效的开发,让常见的失败成为例外,让成功成为常态。
倾听你的测试想要告诉你什么。
软件测试不仅仅是简单的“通过”或“不通过”检查。它就像一座冰山:我们看到的表面部分只是真正发生的情况的一小部分。通过仔细聆听测试反馈,我们可以揭示深层次的设计问题、代码不一致问题,甚至与客户需求不符的问题。
培养解读这些信息的能力对于提高代码质量、工作效率和交付成果的清晰度都至关重要。我们不应该把测试看作技术障碍,而应该将其视为有价值的盟友,能够引导我们开发出更健壮、设计更合理的软件。真正的收益不仅仅是确保代码能够正常运行,还在于理解测试提供的洞察力,以此来改进、调整和完善我们的解决方案。
往期系列文章