是否应该使用单元测试来测试枚举的值?


15

如果您有一个仅带有值的枚举(没有任何方法可以像Java中那样),并且此枚举是系统业务定义的一部分,那么应该为它编写单元测试吗?

我一直认为应该编写它们,即使它们看起来很简单而且很多余,但我认为应该在测试中明确编写与业务规范有关的内容,无论它是使用unit / integration / ui / etc编写的。测试或使用语言的类型系统作为测试方法。因为从业务的角度来看,枚举(例如Java中)必须具有的值不能使用类型系统进行测试,所以我认为应该为此进行单元测试。

这个问题是不是类似于这一个,因为它没有解决同样的问题我的。在该问题中,存在一个业务功能(savePeople),而该人员正在查询内部实现(forEach)。在那里,有一个中间业务层(该功能可节省人员),封装了语言构造(forEach)。这里的语言构造(枚举)是用于从业务角度指定行为的一种。

在这种情况下,实现细节与数据的“真实性质”相吻合,即:(在数学意义上)一组值。您可以说使用了一个不可变的集合,但是相同的值仍然应该在那里存在。如果使用数组,则必须执行相同的操作来测试业务逻辑。我认为这里的难题在于,语言结构与数据的性质非常吻合。我不确定我是否正确解释了自己


19
枚举的单元测试到底是什么样的?
jonrsharpe


@jonrsharpe可以断言枚举内的值是您期望的值。我可以通过遍历枚举的值,并将它们添加到集合(例如,作为字符串)中来实现。订购该套。与测试中手工编写的值的有序列表进行比较的比较。他们应该匹配。
IS1_SO

1
@jonrsharpe,我喜欢将单元测试也视为用代码编写的“定义”或“需求”。枚举的单元测试就像检查枚举中的项数及其值一样简单。特别是在C#中,枚举不是类,而是可以直接映射为整数,从而保证了它们的值并且不能通过巧合进行编程可能对序列化有用。
马查多

2
恕我直言,它并没有比测试2 + 2 = 4来检查宇宙是否正确有用。您使用该枚举而不是枚举本身来测试代码。
Agent_L

Answers:


39

如果您有一个仅带有值的枚举(没有任何方法可以像Java中那样),并且此枚举是系统业务定义的一部分,那么应该为它编写单元测试吗?

不,它们只是状态。

从根本上说,您使用枚举的事实是实现的细节;那就是您可能希望能够重构到不同设计中的一种东西。

测试枚举的完整性类似于测试是否存在所有可表示的整数。

但是,测试枚举支持的行为是一个好主意。换句话说,如果您从通过测试套件开始,并注释掉任何单个枚举值,则至少一个测试应该失败(编译错误被视为失败)。


5
但是在这种情况下,实现细节与数据的“真实本质”相吻合,即:(在数学意义上)一组值。您可以说使用了一个不可变的集合,但是相同的值仍然应该在那里存在。如果使用数组,则必须执行相同的操作来测试业务逻辑。我认为这里的难题在于,语言结构与数据的性质非常吻合。我不确定自己的解释是否正确。
IS1_SO

4
@ IS1_SO —到VOU的一个测试应该失败了:是吗?在这种情况下,您不需要专门测试Enum。不是吗 也许这是一个迹象,表明你可以更简单的代码模型,并通过数据的“真性情”创建一个抽象-例如,无论在甲板上的卡,你真的需要有一个表示[ HeartsSpadesDiamondsClubs如果如果卡是红色/黑色,您只可以刷卡吗?
anotherdave

1
@ IS1_SO可以说您有一个错误代码枚举,并且要抛出null_ptr错误。现在通过枚举有一个错误代码。检查null_ptr错误的代码也通过枚举查找代码。因此它的值可能为5(例如)。现在,您需要添加另一个错误代码。枚举已更改(假设我们在枚举的顶部添加了一个新的枚举)。null_ptr现在的值是6。这有问题吗?您现在返回的错误代码6并测试6。只要一切在逻辑上都是一致的,尽管此更改破坏了您的理论检验,您仍然可以。
Baldrickk

17

您无需测试枚举声明。您可以测试函数输入/输出是否具有预期的枚举值。例:

enum Parity {
    Even,
    Odd
}

Parity GetParity(int x) { ... }

无需编写测试来验证,然后枚举Parity定义名称EvenOdd。这样的测试将毫无意义,因为您只需要重复代码中已经说明的内容即可。重复说两次相同的事情不会使其更正确。

确实编写了测试,验证GetParity说将返回Even0,返回Odd1,依此类推。这很有价值,因为您无需重复执行代码,而是在验证代码的行为,而与实现无关。如果内部代码GetParity被完全重写,则测试仍然有效。确实,单元测试的主要好处是,它们可以确保代码仍按预期运行,从而使您可以自由地安全地重写和重构代码。

但是,如果您要进行测试以确保枚举声明定义了期望的名称,那么将来对枚举所做的任何更改都将要求您也更改测试。这意味着它不仅是工作量的两倍,而且还意味着失去了单元测试的任何好处。如果你在修改代码和测试同时,再有就是对引进的错误没有保障。


为了解决这个问题,我已经更新了我的问题,请检查一下是否有帮助。
IS1_SO

@ IS1_SO:好的,这让我感到困惑-您是在动态生成枚举值,还是发生了什么?
JacquesB

否。我的意思是在这种情况下,选择用来表示值的语言结构是一个枚举。但是我们知道这是一个实现细节。如果选择一个数组,一个Set <>(在Java中)或一个带有一些分隔标记的字符串来表示值,会发生什么情况?如果是这样,那么测试包含的值就是企业感兴趣的值就很有意义。这就是我的意思。这个解释有帮助吗?
IS1_SO

3
@ IS1_SO:您是否在谈论测试从函数返回的枚举实例是否具有一定的期望值?因为是的,您可以测试一下。您只是不需要测试枚举声明本身。
JacquesB

11

如果存在更改枚举会破坏代码的风险,那么可以肯定,C#中具有[Flags]属性的任何事物都是个好例子,因为在2和4(3)之间添加一个值将按位1和2而不是a谨慎的项目。

这是一层保护。

您应该考虑拥有所有开发人员都熟悉的枚举实践代码。不要依赖枚举的文本表示形式,这很常见,但这可能会与您的序列化准则冲突。

我见过人们“纠正”枚举条目的大小写,按字母顺序或通过一些其他逻辑分组对它们进行排序,所有这些都破坏了其他不良代码。


5
如果枚举的数值在任何地方使用(例如存储在数据库中),则重新排序(包括在最后一个值之前删除或插入)可能导致现有记录无效。
stannius

3
+1,这个答案被低估了。如果您的枚举是序列化的一部分,带有外部单词的输入接口或按位组合的信息,则肯定需要对它们进行系统每个版本的一致性测试。至少如果您担心向后兼容,那通常是一件好事。
马查多

11

否,检查枚举包含所有有效值且仅​​执行其他操作的测试实质上是在重复枚举的声明。您只会测试该语言是否正确实现了枚举构造,这是一个毫无意义的测试。

话虽如此,您应该测试取决于枚举值的行为。例如,如果您使用枚举值将实体序列化为json或其他内容,或者将值存储在数据库中,则应测试枚举的所有值的行为。这样,如果枚举被修改,则至少其中一项测试将失败。无论如何,您要测试的是枚举周围的行为,而不是枚举声明本身。


3

您的代码应独立于枚举的实际值正​​常工作。如果是这种情况,则不需要单元测试。

但是您可能拥有更改枚举值会破坏代码的代码。例如,如果枚举值存储在外部文件中,并且在更改枚举值后读取外部文件将给出错误的结果。在这种情况下,您会在枚举附近出现BIG注释,警告任何人不要修改任何值,并且您可能会编写一个检查数值的单元测试。


1

通常,仅检查一个枚举是否具有一组硬编码的值列表就没有多大价值,正如其他答案所说,因为那样的话,您只需要一起更新测试和枚举即可。

我曾经有一个案例,一个模块使用其他两个模块的枚举类型并映射到它们之间。(其中一个枚举具有其他逻辑,另一个枚举用于数据库访问,两个枚举都具有相互隔离的依赖关系。)

在这种情况下,我添加了一个测试(在映射模块中),该测试验证了源枚举中的所有枚举条目也存在于目标枚举中(因此,映射将始终有效)。(在某些情况下,我也进行了其他检查。)

这样,当有人将枚举条目添加到其中一个枚举而忘记将相应的条目添加到另一个枚举时,测试开始失败。


1

枚举只是有限类型,带有自定义(希望有意义)的名称。枚举可能只有一个值,像void只包含null(有些语言称此unit,并且使用的名称void与枚举没有元素!)。它可能有两个值,例如boolhas falsetrue。它有3个,像colourChannelredgreenblue。等等。

如果两个枚举具有相同数量的值,则它们是“同构的”;也就是说,如果我们系统地切换所有名称,则可以使用一个名称代替另一个名称,并且程序的行为也不会有所不同。特别是我们的测试不会有任何不同!

例如,resultwin/ lose/ draw是同构上面colourChannel,因为我们可以代替例如colourChannelresultredwingreenlosebluedraw,只要我们这样做随处可见(生产者和消费者,解析器和串行器,数据库条目,日志文件,等等。 ),那么我们的程序就不会有任何变化。colourChannel即使没有,我们编写的任何“ 测试”仍会通过colourChannel更多!

同样,如果一个枚举包含多个值,我们总是可以重新排列这些值以获得具有相同数量值的新枚举。由于值的数量没有改变,所以新的排列与旧的排列是同构的,因此我们可以切换出所有名称,并且测试仍然可以通过(请注意,我们不能只切换定义;我们必须仍要关闭所有使用网站)。

这意味着,就机器而言,枚举是“可区分的名称”,仅此而已。我们用枚举只能做的事情就是分支两个值是相同的(例如red/ red)还是不同的(例如red/ blue)。这就是“单元测试”唯一可以做的事情,例如

(  red == red  ) || throw TestFailure;
(green == green) || throw TestFailure;
( blue == blue ) || throw TestFailure;
(  red != green) || throw TestFailure;
(  red != blue ) || throw TestFailure;
...

正如@ jesm00所说,这样的测试是在检查语言实现而不是您的程序。这些测试绝不是一个好主意:即使您不信任语言实现,也应该从外部对其进行测试,因为不能信任它正确运行测试!

这就是理论;那练习呢?枚举的这种特征的主要问题在于,“现实世界”程序很少是独立的:我们拥有旧版,远程/嵌入式部署,历史数据,备份,实时数据库等,因此我们永远无法真正“转出”名称的所有出现而不会丢失某些用途。

但是这样的事情不是枚举本身的“责任”:更改枚举可能会中断与远程系统的通信,但是相反地,我们可能会修复通过更改枚举此问题!

在这种情况下,枚举是一条红线:如果一个系统需要采用这种方式,而另一个系统需要采用这种方式,怎么办?无论我们编写多少测试,都不能兼而有之!真正的罪魁祸首是输入/输出接口,该接口应该产生/使用定义明确的格式,而不是“解释选择的整数”。因此,真正的解决方案是测试i / o接口:使用单元测试来检查其是否正在解析/打印所需的格式,而通过集成测试来检查该格式是否真正被另一方接受。

我们可能仍然想知道枚举是否被“充分地执行了”,但是在这种情况下,枚举又是一个红鲱鱼。我们真正关心的是测试套件本身。我们可以通过以下两种方式来建立信心:

  • 代码覆盖率可以告诉我们,来自测试套件的各种枚举值是否足以触发代码中的各个分支。如果没有,我们可以添加触发未覆盖分支的测试,或者在现有测试中生成更多种类的枚举。
  • 属性检查可以告诉我们代码中的各种分支是否足以应付运行时的可能性。例如,如果代码仅处理red,而我们仅使用进行测试red,则覆盖率达到100%。属性检查器将(尝试)生成断言的反例,例如生成我们忘记测试的greenblue值。
  • 变异测试可以告诉我们我们的断言是否实际上检查了枚举,而不是仅仅跟随分支并忽略它们之间的差异。

1

否。单元测试适用于测试单元。

在面向对象的编程中,一个单元通常是一个完整的接口,例如一个类,但是可以是一个单独的方法。

https://zh.wikipedia.org/wiki/Unit_testing

对声明的枚举的自动测试将测试语言及其运行平台的完整性,而不是测试开发人员编写的代码的逻辑。它没有任何用处-包含文档,因为声明枚举的代码与将对其进行测试的代码一样充当文档。


0

您应该测试代码的可观察行为,方法/函数调用对可观察状态的影响。只要代码做对了就可以了,您不需要测试其他任何东西。

您无需明确声明枚举类型具有所需的条目,就像您无需明确声明某个类实际存在或具有所需的方法和属性一样。

实际上,通过测试行为,您隐式地断言测试中涉及的类,方法和值确实存在,因此您无需显式断言。

请注意,您不需要为代码编写有意义的名称即可执行正确的操作,这只是给阅读代码的人们提供了便利。你能与枚举值像你的代码的工作foobar...等方法frobnicate()

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.