在单元测试中挣扎于循环依赖


24

我正在尝试通过使用TDD开发类似于Bit Vector的简单方法来练习TDD。我碰巧正在使用Swift,但这是一个与语言无关的问题。

My BitVector是一个struct存储单个的UInt64,并在其上方提供一个API,可让您将其视为集合。细节无关紧要,但是很简单。高57位是存储位,低6位是“计数”位,它告诉您实际上有多少个存储位存储一个包含的值。

到目前为止,我有一些非常简单的功能:

  1. 构造空位向量的初始化程序
  2. count类型的属性Int
  3. isEmpty类型的属性Bool
  4. 等于运算符(==)。注意:这是类似于Object.equals()Java 的值相等运算符,而不是像==Java中的引用相等运算符。

我遇到了一堆周期性依赖关系:

  1. 测试我的初始化程序的单元测试需要验证新构造的BitVector。它可以通过以下三种方式之一进行操作:

    1. 校验 bv.count == 0
    2. 校验 bv.isEmpty == true
    3. 检查一下 bv == knownEmptyBitVector

    方法1依赖count,方法2依赖isEmpty(它本身依赖count,因此没有用处),方法3依赖==。无论如何,我不能孤立地测试初始化​​程序。

  2. 测试是否count需要对某些东西进行操作,这不可避免地会测试我的初始值设定项

  3. 实施isEmpty依赖count

  4. 执行==依赖count

我能够通过引入一个私有API来部分解决此问题,该API BitVector从现有的位模式(作为UInt64)构造一个。这使我可以在不测试任何其他初始化程序的情况下初始化值,以便可以向上“引导”。

为了使我的单元测试真正成为单元测试,我发现自己做了很多黑客操作,这使我的产品和测试代码大大复杂化。

您如何解决这些问题?


20
您对“单位”一词的看法过于狭窄。BitVector是用于单元测试的完美单位大小,可立即解决您的问题,有BitVector需要的公共成员互相进行有意义的测试。
Bart van Ingen Schenau

您可能会提前了解太多实施细节。您的开发真的受测试驱动吗?
Herby

@herby不,这就是为什么我在练习。尽管这似乎是一个无法实现的标准。我没做过任何事情,都没有对实现所要包含的内容进行清晰的心理估算。
亚历山大-恢复莫妮卡

@Alexander您应该尝试放松一下,否则它将是测试优先的,但不是测试驱动的。只是模糊地说:“我将用一个64位int做一个后备向量作为后备存储”;仅此而已;从这一点开始,TDD进行红绿色重构。实现细节以及API应该来自尝试运行测试(前者),以及首先编写这些测试(后者)。
Herby

Answers:


66

您太担心实现细节了。

在您当前的实现中isEmpty依赖count(或您可能具有的任何其他关系)无关紧要:您应该关心的只是公共接口。例如,您可以进行三个测试:

  • 新初始化的对象具有count == 0
  • 新初始化的对象具有 isEmpty == true
  • 新初始化的对象等于已知的空对象。

这些都是有效的测试,如果您决定重构类的内部结构,使其isEmpty具有不依赖于其他实现的方式,则这些测试就变得尤其重要count-只要您的测试全部通过,您就知道自己还没有回归任何东西。

类似的内容也适用于您的其他观点-请记住测试公共接口,而不是内部实现。在这里,您可能会发现TDD很有用,因为isEmpty在编写任何实现之前,您都要编写所需的测试。


6
@Alexander您听起来像是一个需要明确定义单元测试的人。我所知道的最好的一个来自迈克尔·费瑟斯
canded_orange

14
@Alexander,您将每种方法都视为可独立测试的代码。那就是你困难的根源。如果您整体测试对象而不尝试将其分成较小的部分,这些困难将消失。对象之间的依赖性不能与方法之间的依赖性相提并论。
阿蒙

9
@Alexander“一段代码”是任意度量。仅通过初始化变量,您就可以使用许多“代码段”。要紧的是,你正在测试一个凝聚力的行为单位被定义
蚂蚁P

9
“从我读过的书中,我得到的印象是,如果只破坏一段代码,则只有与该代码直接相关的单元测试应该会失败。” 这似乎是一个硬性的规则可循。(例如,如果您编写了一个向量类,并且在索引方法上出错,则使用该向量类的所有代码可能会
遭受大量破坏

4
@Alexander另外,查看“安排,执行,声明”模式以进行测试。基本上,您将对象设置为处于所需的任何状态(“安排”),调用您实际测试的方法(“执行”),然后验证其状态是否根据您的期望进行了更改。(断言)。您在“安排”中设置的内容将成为测试的“前提条件”。
GalacticCowboy

5

您如何解决这些问题?

您对什么是“单元测试”进行了修改。

从根本上说,管理内存中可变数据的对象是状态机。因此,任何有价值的用例都将至少调用一种方法将信息放入对象中,并调用一种方法从对象中读取信息的副本。在有趣的用例中,您还将调用其他更改数据结构的方法。

实际上,这通常看起来像

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

要么

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

“单元测试”术语-嗯,它长期以来一直不是很好。

我称它们为单元测试,但它们与单元测试的公认定义不太吻合-Kent Beck,示例驱动开发

肯特(Kent)在1994年编写了SUnit的第一个版本,在1998年向JUnit移植,在2002年初编写了TDD书的初稿。这种混乱有很多时间可以传播。

这些测试(更准确地称为“程序员测试”或“开发人员测试”)的关键思想是这些测试相互隔离。这些测试不共享任何可变的数据结构,因此它们可以同时运行。不必担心测试必须以特定顺序运行才能正确衡量解决方案。

这些测试的主要用例是,它们由程序员在对其自己的源代码进行编辑之间运行。如果您执行的是红色绿色重构协议,则意外的RED总是表示您上次编辑存在错误;您还原该更改,确认测试为绿色,然后重试。尝试投资仅通过一项测试即可捕获每个可能的错误的设计并没有很多优势。

当然,合并会引入故障,然后发现故障不再是无关紧要的。您可以采取各种步骤来确保故障易于定位。看到


1

通常(即使不使用TDD),您也应努力编写尽可能多的测试,同时假装不知道如何实现。

如果您实际上在进行TDD,那应该已经是事实了。您的测试是程序的可执行规范。

只要测试本身合理且维护良好,调用图在测试下方的外观就无关紧要。

我认为您的问题是您对TDD的了解。

我认为您的问题是您正在“混合” TDD角色。理想情况下,您的“测试”,“代码”和“重构”角色完全独立于彼此运行。特别是,您的编码和重构角色没有使测试保持绿色运行的义务。

当然,原则上最好是所有测试都是正交且彼此独立的。但这不是您的其他两个TDD角色所关心的问题,并且绝对不是测试的严格甚至是现实的硬性要求。基本上:不要丢掉您对代码质量的常识,以尝试满足没人要您的要求。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.