测试驱动陷阱,第1部分

您是否曾经遇到过这样的情况:简单的代码更改就破坏了几百个测试? 您是否曾经想到过测试会降低您的速度,抑制您的创造力,使您害怕更改代码。 如果您有,那就意味着您已经进入了非常糟糕的地牢测试,不该进行的事情世界。

我去过那儿。 我自己建造了一个。 崩溃使我丧命。 我学到了教训。 这就是一个死人的故事。 从我的错误中学习或注定要重蹈覆辙。

故事

就像世界上所有好的游戏一样,“测试驱动开发”很容易学习,很难掌握。 我从2005年开始,当时一个名叫Piotr Szarwas的杰出人物给我写了《测试驱动开发:以示例为例》(肯特·贝克)的书,其中一项任务是:创建框架。

在过去,当我们使用的技术根本没有框架时,我们想要一个像Spring这样酷的框架,它具有控制反转,对象关系映射,模型-视图-控制器等所有优点我们知道。 因此,我们创建了一个框架。 然后,我们在此基础上构建了一个内容管理系统。 然后,我们在这两个应用程序之上为不同的客户,Internet商店和其他应用程序创建了一堆专用应用程序。 我们做得很好。 我们对该框架进行了3000多次测试,对于CMS进行了3000多次测试,对于每个专用应用程序,我们进行了数千次测试。 我们正在看我们的工作,我们感到高兴,安全,有保障。 这些是美好的时光。

然后,随着我们代码库的增长,我们来到了一个点,当时我们拥有的一个简单的贫血模型已经不够好了。 那时,我还没有读过另一本重要的书:“域驱动设计”。 我还不知道,只有贫血的模型才可以做到这一点。

但是我们很安全。 我们进行了大量的测试。 我们可以改变任何东西。

还是我想。

我花了一个星期的时间尝试介绍该体系结构中的一些更改。 真正简单的事情是:移动方法,切换协作者等。 只是让我不得不修复的测试数量不知所措。 那是TDD,我从编写测试开始了我的更改,当我最终完成了测试下的代码时,我发现另外几百个测试完全被我的更改破坏了。 当我修复它们后,在过程中进行了更多更改时,我发现又有几千个损坏。 那是蝴蝶效应,是由很小的变化引起的连锁反应。

我花了一个星期才弄清楚,我在这里甚至还没有完成一半。 重构没有明显的目的。 而且我的代码库从来没有稳定,可以部署。 我的分支机构位于存储库中,我已将其重命名为“ Lasciate ogne speranza,voi ch'intrate”。

我们进行了无数次测试。 非常糟糕的测试。 测试会使我们的代码具体化,因此我们什么也做不了。

唯一真正的选择是:要么保留它,要么删除所有测试,然后再次从头开始编写所有内容。 如果我们选择第一个选项,我不想使用代码,并且管理层也找不到第二个选项的财务理由。 所以我退出了。

那是我建造的地牢,却发现自己被它的怪物打败了。

我回到书上,发现在那里做错的一切。 概述。 标出。 我该如何跳过呢? 我怎么可能不注意到? 事实证明,有时候,您需要年龄和经验,才能真正了解您所学到的东西。

即使是最好的工具,如果使用不当,也会对您不利。 而且该工具越容易使用,似乎越容易使用,就越容易陷入“我知道如何工作”的陷阱。 然后是BAM! 你已经走了。

真相

测试驱动开发和测试是完全不同的两件事。 测试只是TDD的副产品,仅此而已。 TDD的意义是什么? TDD带来什么? 我们为什么要进行TDD?

因为三个,而且只有这三个原因。

1.通过将自己置于用户的鞋子中来找到最佳设计。 从“我想如何使用它”的思维开始,我们发现了最有用和最友好的设计。 总是很好,很多时候这是最好的设计。 否则,我们得到的是:

而且你不想要那样。

2.处理我们的恐惧。 它需要花很多时间才能在没有测试的情况下在大型代码库中进行根本性的改变,然后说“完成了”而又没有在过程中引入错误,不是吗? 好吧,事实是,如果您说“完成了”,那么大多数时候您要么无知,鲁re,要么只是愚蠢。 就像并发一样:每个人都知道,没有人能做得很好。

聪明的人害怕这种变化。 除非他们有良好的测试,否则代码覆盖率很高。

TDD通过向我们提供证据证明事情可以按预期进行,从而解决了我们的恐惧。 TDD给我们安全

3.获得快速反馈。 您可以在不运行应用程序的情况下编码多长时间? 您可以在不知道自己的代码是否按预期工作的情况下编码多长时间?

测试中的反馈很重要。 对于前端编程而言,情况就不那么如此了,您可以在其中进行修改,然后自己看看。 有关在后端进行编码的更多信息。 如果您的技术堆栈需要编译,部署和启动,甚至更多。

时间就是金钱,我宁愿赚钱,也不愿等待部署并每次更改时都单击我的更改。

就是这样。 完全没有理由使用TDD。 我们想要好的设计,安全和反馈。 好的测试就是那些,这给了我们这些。

测试不好?

所有其他测试都不好。

坏习惯

那么,典型的不良测试是什么样的呢? 我在一个几乎每个项目中都反复看到的那个,它是由一个尚未学会如何不构建丑陋的地牢,如何不将具体内容倾倒在您的代码库上的人创建的。 我在2005年写给自己的那本书。

这将是一个用Groovy编写的Spock示例,用于测试Grails控制器。 但是,如果您不了解这些技术,请不要担心。 我敢打赌,您会毫无疑问地了解其中的情况。 是的,就是这么简单。 我将解释所有不太明显的部分。

def 'should show outlet'() {
  given:
    def outlet = OutletFactory.createAndSaveOutlet(merchant: merchant)
    injectParamsToController(id: outlet.id)
  when:
    controller.show()
  then:
    response.redirectUrl == null
}

所以我们有一个控制器。 这是一个插座控制器。 我们有一个测试。 这个测试怎么了?

测试的名称是“应该显示出口”。 具有这种名称的测试应该检查什么? 我们是否显示出口,对不对? 它会检查什么? 是否重定向。 辉煌? 无用。

这很简单,但是我发现它到处都是。 人们忘记了,我们需要:

验证正确的事情

我敢打赌,测试是在代码之后编写的。 并非以测试优先的方式。

但是,仅仅验证正确的事情还不够。 让我们再举一个例子。 相同的控制器,不同的期望。 名称为:“应使用新帐户使用有效的参数创建插座插入命令”

很复杂,不是吗? 如果您需要解释,则名称错误。 但是您不知道该域,因此让我对其进行一些说明:在为控制器提供良好的参数时,我们希望它创建一个新的OutletInsertCommand,并且该帐户应该是新的。

该名称并未说明“新”是什么,但我们应该能够在代码中看到它。

看一下测试:

def 'should create outlet insert command with valid params with new account'() {
  given:
    def defaultParams = OutletFactory.validOutletParams
    defaultParams.remove('mobileMoneyAccountNumber')
    defaultParams.remove('accountType')
    defaultParams.put('merchant.id', merchant.id)
    controller.params.putAll(defaultParams)
  when:
    controller.save()
  then:
    1 * securityServiceMock.getCurrentlyLoggedUser() >> user
    1 * commandNotificationServiceMock.notifyAccepters(_)
    0 * _._
    Outlet.count() == 0
    OutletInsertCommand.count() == 1
    def savedCommand = OutletInsertCommand.get(1)
    savedCommand.mobileMoneyAccountNumber == '1000000000000'
    savedCommand.accountType == CyclosAccountType.NOT_AGENT
    controller.flash.message != null
    response.redirectedUrl == '/outlet/list'
}

如果您不熟悉Spock:n * mock.whatever(),表示模拟对象的“无论”方法应精确调用n次。 不多不少。 下划线“ _”表示“所有”或“任何”。 >>符号指示测试框架在调用该方法时返回右侧参数。

那么这个测试有什么问题呢? 几乎所有的东西。 让我们从“然后”部分的开头开始,认真地跳过“给定”中过分的设置。

1 * securityServiceMock.getCurrentlyLoggedUser() >> user

第一行验证是否要求某些安全服务提供登录用户,然后返回该用户。 恰好有人问过。 不多不少。

等一下 我们怎么会在这里提供安全服务? 测试的名称未涉及安全性或用户的任何内容,为什么我们要对其进行检查?

好吧,这是第一个错误。 这部分不是,我们要验证。 控制器可能需要这样做,但这仅意味着它应该在“给定”中。 并且它不应该验证它是否被称为“恰好一次”。 看在上帝的份上。 用户是否登录。 让他“登录,但您只能询问一次”是没有意义的。

然后,有第二行。

1 * commandNotificationServiceMock.notifyAccepters(_)

它验证某些通知服务仅被调用一次。 没关系,业务逻辑可能会要求这样做,但是然后…为什么在测试名称中没有明确说明呢? 啊,我知道,这个名字太长了。 好吧,这也是一个建议。 您需要进行另一项测试,例如“应该通知新创建的插座插入命令”。

然后,这是第三行。

0 * _._

我最喜欢的一个 如果代码是Han Solo,则此行是小屋Jabba。 它希望汉斯·索洛(Hans Solo)冻结在坚固的混凝土中。 还是死了。 或两者。

如果您尚未扣除这一行,则为:“您不得与任何模拟或存根或任何其他东西进行任何其他交互,阿们!”。

那是我一段时间以来最愚蠢的事情。 为什么一个理智的程序员曾经把它放在这里? 那超出了我的想象。

不,不是。 去过也做过。 程序员之所以使用这样的东西,是为了确保他涵盖了所有交互。 他没有忘记任何事情。 测试是好的,拥有更多的东西有什么不好呢?

他忘了理智。 那条线是愚蠢的,并且会有复仇的感觉。 有一天,它会咬你的屁股。 尽管它可能很小,但由于有成百上千条这样的线,所以总有一天您会被咬得很好。 您可能还无法生存。

然后,另一行。

Outlet.count() == 0

这验证了我们在数据库中是否没有出口。 你知道为什么吗? 你不知道 我做。 我这样做是因为我知道该域的业务逻辑。 您不是因为此测试很容易通知您,应该通知您什么。

然后是实际上有意义的部分。

OutletInsertCommand.count() == 1
    def savedCommand = OutletInsertCommand.get(1)
    savedCommand.mobileMoneyAccountNumber == '1000000000000'
    savedCommand.accountType == CyclosAccountType.NOT_AGENT

我们期望在数据库中创建的对象,然后验证其帐户是否为“新”。 我们知道,“新”是指特定的帐号和类型。 尽管它被提取为另一种方法而尖叫。

然后…

controller.flash.message != null
    response.redirectedUrl == '/outlet/list'

然后,我们有一些未设置的即时消息。 和重定向。 我问上帝,为什么我们要对此进行测试? 不是因为测试名称如此,那是肯定的。 事实是,看着测试,我可以逐行重新创建被测方法。

这不是很聪明吗? 该测试代表了一种不太简单的方法的每一行。 但是,尝试更改方法,尝试更改单行,您就有很大的机会炸毁该东西。 当这些测试成百上千时,您将在代码中遍及所有具体对象。 您将无法进行任何重构。

所以这是另一堂课。 验证正确的事情还不够。 你需要

仅验证正确的内容。

绝对不要逐步验证方法的算法。 验证算法的结果。 只要结果没有改变,您应该自由地更改方法,只要您期望的真实结果不变即可。

想象一个排序问题。 您会验证它的内部算法吗? 做什么的? 它必须工作,并且必须工作良好。 请记住,您想要好的设计和安全性。 除此之外,它应该可以自由更改。 您的测试不应妨碍您。

现在再举一个可怕的例子。

@Unroll('test merchant constraints field #field for #error')
def 'test merchant all constraints'() {
  when:
    def obj = new Merchant((field): val)

  then:
    validateConstraints(obj, field, error)

  where:
    field                     | val                                    | error
    'name'                    | null                                   | 'nullable'
    'name'                    | ''                                     | 'blank'
    'name'                    | 'ABC'                                  | 'valid'
    'contactInfo'             | null                                   | 'nullable'
    'contactInfo'             | new ContactInfo()                      | 'validator'
    'contactInfo'             | ContactInfoFactory.createContactInfo() | 'valid'
    'businessSegment'         | null                                   | 'nullable'
    'businessSegment'         | new MerchantBusinessSegment()          | 'valid'
    'finacleAccountNumber'    | null                                   | 'nullable'
    'finacleAccountNumber'    | ''                                     | 'blank'
    'finacleAccountNumber'    | 'ABC'                                  | 'valid'
    'principalContactPerson'  | null                                   | 'nullable'
    'principalContactPerson'  | ''                                     | 'blank'
    'principalContactPerson'  | 'ABC'                                  | 'valid'
    'principalContactInfo'    | null                                   | 'nullable'
    'principalContactInfo'    | new ContactInfo()                      | 'validator'
    'principalContactInfo'    | ContactInfoFactory.createContactInfo() | 'valid'
    'feeCalculator'           | null                                   | 'nullable'
    'feeCalculator'           | new FixedFeeCalculator(value: 0)       | 'valid'
    'chain'                   | null                                   | 'nullable'
    'chain'                   | new Chain()                            | 'valid'
    'customerWhiteListEnable' | null                                   | 'nullable'
    'customerWhiteListEnable' | true                                   | 'valid'
    'enabled'                 | null                                   | 'nullable'
    'enabled'                 | true                                   | 'valid'
}

你知道发生了什么吗? 如果您以前从未看过它,那么您很可能不会看到。 “ where”部分是用于参数化测试的漂亮Spock解决方案。 这些列的标题是第一行之前使用的变量名称。 使用后有点像声明。 该测试将被触发多次,对于“ where”部分中的每一行都会触发一次。 而这一切都归功于Groovy的Abstract Syntaxt Tree Transfrmation。 我们正在谈论在编译过程中解释和更改代码。 酷的东西。

那么这个测试在做什么呢?

没有。

让我向您展示正在测试的代码。

static constraints = {
  name(blank: false)
  contactInfo(nullable: false, validator: { it?.validate() })
  businessSegment(nullable: false)
  finacleAccountNumber(blank: false)
  principalContactPerson(blank: false)
  principalContactInfo(nullable: false, validator: { it?.validate() })
  feeCalculator(nullable: false)
  customerWhiteListEnable(nullable: false)
}

这个静态的关闭告诉Grails,我们期望在对象和数据库级别进行哪种验证。 在Java中,这些很可能是注释。

并且您不测试注释。 您也不会测试静态字段。 或没有任何明智代码,没有任何行为的闭包。 而且,您无需测试下面的框架(此处为Grails / GORM)是否可以正常工作。

哦,您可能是第一次使用它进行测试。 只是因为您想知道它如何以及是否有效。 毕竟,您要安全。 但是,您可能应该删除此测试,并且可以肯定的是,不要对那里的每个单个域类重复进行此测试。

顺便说一句,该测试不会验证该事件。 因为它是一个单元测试,所以可以处理数据库的模拟。 它并没有测试真正的GORM(Groovy对象关系映射,Hibernate之上的适配器)。 它正在测试真实GORM的模拟。

是的,就是那么愚蠢。

因此,如果TDD为我们提供安全性,设计和反馈,那么该测试将提供什么? 绝对没有。 那程序员为什么要把它放在这里呢? 因为他的大脑说:测试很好。 测试越多越好。

好吧,我有消息要告诉你。 每项不能为我们提供安全性和良好设计的测试都是不好的。 期。 当您停止在测试下重构代码时,应仅丢弃那些仅提供反馈的功能。

这是我的第三课:

提供安全性和良好的设计,否则就会消失。

那是出问题的例子。 我们应该怎么做?

答案:删除它。

但是我还必须看到一个删除测试的程序员。 甚至如此糟糕。 我想我们对我们的代码非常个人化。 因此,如果您犹豫不决,让我提醒您肯特·贝克(Kent Beck)在他的有关TDD的书中写道:

测试的第一个标准是信心。 如果它降低了您对系统行为的信心,请不要删除它。

第二个标准是沟通。 如果您有两个测试在代码中使用相同的路径,但是对于读者来说它们代表不同的场景,请不要理会它们。

[Kent Beck,测试驱动开发:示例
现在您知道了,将其删除是安全的。

今天这么多。 我有一些好的例子要展示,还有更多的故事要讲,请继续关注第二部分

参考: 测试驱动陷阱,来自我们JCG合作伙伴 Jakub Nabrdalik的第1部分 ,在Solid Craft博客上。


翻译自: https://www.javacodegeeks.com/2012/09/test-driven-traps-part-1.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值