吃饭、睡觉、呼吸单元测试/TDD/BDD

29

我在编写API和核心功能时会编写单元测试。但我想成为一个粉丝,热爱TDD和BDD。那么正确开始学习TDD/BDD的最佳方法是什么?有没有书籍、资源、框架和最佳实践?

我的环境是Java后端与Grails前端集成,同时集成了多个外部Web服务和数据库。


2
测试网页很困难。你不可避免地需要测试视图代码,而许多人试图通过在原始HTML中测试字符串的存在来做到这一点。这只会弄乱代码。更好的方法是使用类似Selenium的集成测试来主要测试视图代码。 - Bob Aman
12个回答

25
一个好的起点是阅读博客,然后购买那些写博客的人所写的书籍。我强烈推荐以下几本书:
"大叔" Bob Martin 和 Object Mentor 的团队: http://blog.objectmentor.com/ P.S. 购买 Bob 的《Clean Code》一书: http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882 我的朋友 Tim Ottinger(前 Object Mentor 的成员) http://agileinaflash.blogspot.com/ http://agileotter.blogspot.com/ Jetbrains 公司的团队: http://www.jbrains.ca/permalink/285 我觉得有必要详细说明一下,因为其他人似乎只想给你们他们对 TDD 的看法,而不愿意帮助你们成为绝地忍者。TDD 的迈克尔·乔丹是 Kent Beck。他确实写了这方面的专著: http://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530 他也有博客: http://www.threeriversinstitute.org/blog/?p=29 还有其他的 TDD 的 "名人支持者",包括:

这些都是值得关注的伟大人物。你也应该考虑参加一些会议,比如Agile 2010或者软件工艺大会(今年它们在芝加哥同时举行)。


http://agileinaflash.blogspot.com/2009/02/meaningful-names.html 是这篇文章的简称。是的,Ryber是我的朋友。 - Tim Ottinger

11
我不喜欢人们说“练习X从来不会有坏处;如果不起作用,你就没有做对。”抱歉,它和其他过度热情的宗教教条感觉一样。我不相信。
我同意那些认为最好的解决方案应该是你的时间和金钱所负担得起的人的看法。
反对测试驱动开发的人不应该自动被指责忽视质量。("那么你什么时候停止打你的妻子了?") 事实是软件中存在错误,消除所有错误的成本必须与收益权衡。
制造业也是如此。尺寸公差和表面光洁度不都相同,因为有时候不需要一个严格的尺寸公差和镜面光洁度。
是的,我编写单元测试,虽然不经常在编写类之前。我看到测试对设计的影响。我测量并观察代码覆盖率。如果发现我的覆盖率不足,我会编写更多的测试。我理解单元测试的安全网对于重构的好处。即使我独自工作,我也遵循这些实践,因为我亲身体验过它们的好处。我明白它的好处。
但是,如果我的队友开始纠缠我“吃饭、睡觉和呼吸单元测试和TDD”,我会怀疑他们。
“我的经理说,唯一能让我升职的方法就是让团队采用TDD / BDD。”
你有没有想过这让你听起来像个马屁精?你发现自己的唠叨已经疏远了你的团队吗?
这种回应可能会让我失去一些声誉分,但必须得说。
我认为更好的方法是先自己做好,让他人看到好处。以身作则。这比空口白话更具有说服力。
天啊,Grails内置测试生成。如果你正在使用Grails的团队中工作,还需要多少推销呢?

+1 说这个不会扣分。我只是在讽刺我的经理比产品质量更着迷于TDD这个流行词。 - Langali
抱歉,我的浏览器过滤了讽刺和嘲讽。我误解了你的意图。 - duffymo
取决于他们反对测试驱动开发的方式。如果你是“吃饭、睡觉、呼吸”测试,我会说你的重点放错了。但是TDD不是测试。TDD是整个开发过程,假设你不介意你的整个生活都是开发,那么我对那些说他们想要“吃饭、睡觉和呼吸”TDD的人没有任何问题。尽管有讽刺意味。 - Bob Aman
顺便说一句,有很多问题域对于 TDD 实际上并不适用。例如游戏开发。你可以对游戏中的大量内容进行单元测试,但是仍然有大量代码几乎无法进行测试。这对 TDD 造成了阻碍。 - Bob Aman
1
我的个人经验是,在库代码上使用TDD比在Web应用程序代码上使用TDD获得的结果要好3倍(这是我工作的两个主要领域),因此可以说TDD的好处并不是均匀分布的。不同的应用程序有不同的测试需求。然而,我还没有遇到过觉得TDD浪费时间的项目。(至少自从我开始做好TDD以来是这样的;一开始它有点麻烦。) - Bob Aman
显示剩余2条评论

6

我的最佳实践建议是:做实际可行的事情,而不仅仅是因为它是一个流程。不要忘记编写应用程序的目标,在商业世界中,它并不是编写测试。不要误解我,测试有其重要性,但这不应该成为目标。


2
从来没有做过好的TDD的人总是给出这样的建议。有时候他们做过,但进一步了解后,很明显他们所做的更像是糟糕的TDD,而且他们大部分的问题源于不良实践,而不是TDD本身。 - Bob Aman
你说中了,我想我从来没有真正做过“真正的”TDD。 实际上,我的意思是当正确使用时,它们确实提供价值,但您必须小心,确保您的重点是提供解决方案,而不仅仅是编写测试。 - user101884
涵义与字面意思一样重要。你说的是事实,但这只能算是值得称赞的部分。即使是最坚定的TDD支持者也不认为“测试是目标”。我称之为草人论法。目标是可靠的代码,低维护成本和短开发时间。测试可以帮助你实现这个目标,如果你正确地进行TDD,那么在开始看到附加测试的收益递减之前,你必须达到80%以上的覆盖率。实际上,这基本上意味着你编写的每个测试都会在长期内提高你的生产力。 - Bob Aman
2
依我之见(以及许多比我聪明的人的意见),如果你没有测试,那么你根本没有可用的代码。测试不是一种好的选择,而是必要和必需的。除非你喜欢拥有漫长的开发周期、无法理解其工作原理的错误代码和整个QA团队。 - ryber
2
天啊,我们在使用TDD之前都做了些什么呢? - user101884
2
@wilums2 在我们有汽车之前,我们使用马车。不多的人会说马车比汽车更有效 :) - Thomas Winsnes

6

找一个有 TDD/BDD 经验的人,与他们一起进行配对编程。


5
我已经进行TDD几年了,但最近我开始更深入地研究BDD的设计和开发方式。帮助我入门BDD的资源首先是Dan North的博客(BDD的“创始人”)。请看Introducing BDD。还有一个“官方”的BDD Wiki在behaviour-driven.org上,其中有一些值得阅读的好文章。
当我开始使用BDD(现在仍然有点困难)时,我发现最困难的一件事情是如何制定那些适合BDD的场景。Scott Bellware是一位擅长BDD(或者他喜欢称之为上下文规范化)的专家,他在Code Magazine中的文章Behavior-Driven Development对我理解BDD的思维方式和制定用户故事有很大帮助。
我还推荐TekPub的视频教程Behavior-driven Design with Specflow,由Rob Conery制作。这是一个非常好的BDD入门教程,同时介绍了一个非常适合用于C#的BDD工具(SpecFlow)。
至于TDD资源,这里已经有很多好的推荐了。但我想指出几本我真正推荐的书籍:

5
指标是我个人认为从这里到那里的最佳方式。跟踪代码覆盖率、每次提交的代码复杂度增量,使用可以监视您的代码变化并不断重新运行相应测试的测试运行程序。永远不要让测试长度超过几行,以便所有工具都能正常工作。我建议每月休息一天,运行您的代码经过变异测试器。该日应专门用于编写测试。如果您没有良好的TDD习惯,所有这些东西都会给您带来痛苦。从痛苦中学习,并在很短时间内做得正确。
而且永远不要忘记测试的目的:描述所需行为。它们是您的可执行规范。(这也是为什么我喜欢Cucumber;现在您可以让您的PHB为您编写测试!可能还没那么好,但接近了!)

3
永远不要忘记测试的目的是什么:描述期望的行为。 - Langali
你能推荐一款变异测试工具吗? - Thorbjørn Ravn Andersen
1
分别为Ruby、Python和Java提供的Heckle、Pester和Jester。 - Bob Aman
+1 对于 Cucumber 链接,非常有趣! - Thorbjørn Ravn Andersen

5

PS:我的经理说,唯一能让我升职的方法是让团队实行TDD/BDD。

让一个团队改变他们的习惯(而你不会被同时搞崩)的唯一现实途径是向他们清晰地展示改变将对他们有益。也就是说,编写代码。写很多代码。大量的代码。当接收到重要的电子邮件改变了规格后,展示给他们你如何通过重构轻松修改代码,更糟糕的是因为你预先准备好了测试,所以你能够应对。绿色的条纹,hack hack hack,红色的条纹!!!,hack hack hack,绿色的条纹,然后回家。

阅读Kent Beck关于测试驱动设计的书籍。从测试开始,然后再去写代码。启动一个运行测试的构建服务器!你不需要让整个团队使用它-只需为自己操作并向他们展示它的有效性。

只讲道理只会惹恼本地人 :)


2

首先进行单元测试,然后了解如何正确地执行单元测试,最后教授你的团队TDD,并让他们加入其中-因为根据我的经验,没有什么比与整个团队一起进行单元测试更重要的事情了。

您还需要一个适当的构建过程-使用构建服务器来构建代码并运行测试,我建议使用TeamCity(带有限制的免费版本)。

学习如何编写良好的单元测试是困难的部分-你可以从互联网上搜索到其中的一些内容,但只要您坚持进行单元测试,您也会自己学到更多。

当不编写单元测试作为开发的一部分看起来很奇怪时,您就知道已经达到了目标。


我们确实有一个构建过程和一些测试,但这些都是外包的。 - Langali

2

记住,敏捷开发意味着你并不完全信奉任何特定的方法。如果你正在处理一些 TDD 的好处不值得使用的东西(比如在 Swing 界面上进行试错式编辑),那就不要使用 TDD。


TDD是否总是必须成为敏捷开发的一部分? - Langali
我曾认为敏捷开发意味着我们没有书面要求,项目经理可以随时来到我的桌子前,要求我添加这个小功能或那个小功能。 - JimN
哈哈,这正是一些项目经理想要的。但事实并非如此。 - Kaleb Brasee

1
一年前,我对如何进行TDD(但真的很想学)一无所知,也从未听说过BDD...现在我都强迫自己做这两个。我一直在.Net开发环境中工作,而不是Java,但我甚至用宏替换了“F5-运行”按钮,以便根据是特性/场景还是规范来运行Cucumber(BDD)或MBUnit(TDD)。尽可能不使用调试器。如果你使用调试器,就要往罐子里放1美元(开玩笑(有点认真))。
这个过程非常棒。我们还使用了我很幸运遇到并吸收信息的The Oracle框架。
一切都始于BDD。我们的BDD是基于Iron Ruby的Cucumber。
特性:
场景:.... 假设我做某事... 当我做其他事情时... 然后奇妙的事情发生了...
场景:...
这不是单元测试本身,但它驱动特性,逐个场景地进行,进而驱动单元(测试)规范。因此,您从一个场景开始,每完成一个步骤,就会驱动您的TDD。

我们一直在使用的TDD在某种程度上类似于BDD,因为我们关注SUT(被测试系统)需要的行为,并且每个规范(类“test”文件)指定一个行为。

例如:

这是一个行为的规范:当创建SUT时。

还有一个规范(C# When_blah_happens类文件)用于另一个行为,即当属性更改时,但它被分成一个单独的文件。

using MavenThought.Commons.Testing;
using SharpTestsEx;

namespace Price.Displacement.Module.Designer.Tests.Model.Observers
{
    /// <summary>
    /// Specification when diffuser observer is created
    /// </summary>
    [ConstructorSpecification]
    public class When_diffuser_observer_is_created
        : DiffuserObserverSpecification
    {
        /// <summary>
        /// Checks the diffuser injection
        /// </summary>
        [It]
        public void Should_return_the_injected_diffuser()
        {
            Sut.Diffuser.Should().Be.SameInstanceAs(this.ConcreteDiffuser);
        }
    }
}

这可能是SUT最简单的行为,因为在这种情况下,当它被创建时,Diffuser属性应该与注入的扩散器相同。我不得不使用具体的扩散器而不是模拟,因为在这种情况下,扩散器是一个核心/域对象,并且没有接口的属性通知。95%的时间我们像Dep()一样引用所有依赖项,而不是注入真正的东西。

通常我们有多个[It] Should_do_xyz(),有时会有相当多的设置,比如最多10行存根。这只是一个非常简单的例子,在该规范中没有GivenThat()或AndGivenThatAfterCreated()。

对于每个规范的设置,我们通常只需要覆盖规范的几个方法:

GivenThat() ==> 在创建SUT之前发生。

CreatSut() ==> 我们使用StructureMap自动模拟sut的创建,90%的时间不需要覆盖此方法,但如果您正在构造函数注入Concrete,则必须覆盖此方法。

AndGivenThatAfterCreated() => 在创建SUT之后发生。

WhenIRun() => 除非它是[ConstructorSpecification],否则我们使用它来运行一个代码行,这是我们为SUT指定的行为

此外,如果同一SUT的两个或多个规范具有共同的行为,则将其移动到基本规范中。

要运行规范,我只需突出显示其名称,例如"When_diffuser_observer_is_created",然后按F5,因为请记住,对于我来说,F5会运行Rake任务,无论是test:feature[tag](如果是Cucumber)还是test:class[SUT]。对我来说很有意义,因为每次运行调试器时都是一次性的,不会创建任何代码(哦,而且它花费1美元(开玩笑))。

这是使用TDD指定行为并拥有非常简单的SUT和简单规范的非常干净的方法。如果您尝试成为牛仔编码人员并编写具有硬依赖关系等糟糕的SUT,则会感受到尝试进行TDD并感到沮丧/放弃或咬紧牙关并做正确的事情的痛苦。

以下是实际的SUT。我们使用了PostSharp来在Diffuser上添加属性通知更改,因此使用了Post.Cast<>。这就是为什么我注入了一个Concrete而不是Mock的原因。无论如何,您可以看到另一个规范中定义的缺失行为是当Diffuser上的任何内容发生更改时。

using System.ComponentModel;
using MavenThought.Commons.Events;
using PostSharp;
using Price.Displacement.Core.Products;
using Price.Displacement.Domain;

namespace Price.Displacement.Desktop.Module.Designer.Model.Observers
{
    /// <summary>
    /// Implementation of current observer for the selected product
    /// </summary>
    public class DiffuserObserver : AbstractNotifyPropertyChanged, IDiffuserObserver
    {
        /// <summary>
        /// gets the diffuser
        /// </summary>
        public IDiffuser Diffuser { get; private set; }

        /// <summary>
        /// Initialize with a diffuser
        /// </summary>
        /// <param name="diffuser">The diffuser to observe</param>
        public void Initialize(IDiffuser diffuser)
        {
            this.Diffuser = diffuser;
            this.NotifyInterface().PropertyChanged += (x, e) => this.OnPropertyChanged(e.PropertyName);
        }

        /// <summary>
        /// Gets the notify interface to use
        /// </summary>
        /// <returns>The instance of notify property changed interface</returns>
        protected INotifyPropertyChanged NotifyInterface()
        {
            return Post.Cast<Diffuser, INotifyPropertyChanged>((Diffuser)Diffuser);
        }
    }
}

总之,这种BDD/TDD开发模式非常棒。虽然花了一年时间,但我已经完全转变成了这种生活方式的信奉者。如果不是通过The Oracle http://orthocoders.com/,我绝对无法学到这个。
红色或蓝色的药丸,选择权在你手中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接