在测试和生产代码之间复制常量?


20

在测试和真实代码之间复制数据是好是坏?例如,假设我有一个Python类FooSaver,该类将具有特定名称的文件保存到给定目录:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

现在,在我的测试中,我想确保所有这些文件均已创建,因此我想说一下这样的话:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

尽管这会在两个位置上复制文件名,但我认为这很好:它迫使我准确写下我希望输出的另一端内容,它增加了防止错别字的保护层,并且通常使我对一切正常充满信心完全符合我的期望。我知道,如果将来更改a.foo_filetype_a.foo_file,则必须在测试中进行一些搜索和替换,但是我认为这没什么大不了的。如果我忘记更新测试以换取确保我对代码和测试的理解是同步的,我宁愿有一些误报。

一位同事认为这种重复是不好的,因此建议我将双方重构为以下形式:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

并在测试中:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

我不喜欢这样,因为它不能使我确信代码是否按预期工作了---我只是out_dir + name在生产端和测试端重复了这一步骤。在理解+字符串的工作方式时,它不会发现错误,也不会捕获错别字。

另一方面,它比两次写出这些字符串要容易得多,并且在这样的两个文件之间复制数据似乎有点不对劲。

这里有明确的先例吗?是否可以在测试和生产代码之间复制常量,还是太脆弱了?

Answers:


16

我认为这取决于您要测试的内容,这取决于类的约定。

如果类的契约恰好是在特定位置FooSaver生成的a.foo_fileb.foo_file则应直接对其进行测试,即在测试中复制常量。

但是,如果类的约定是在一个临时区域中生成两个文件,每个文件的名称很容易更改,尤其是在运行时,则必须更改它们的名称,那么您必须更通用地进行测试,可能使用测试之外的常量。

因此,您应该从更高的领域设计角度与您的同事就类的真正性质和契约进行争论。如果您不同意,那么我会说这是类本身的理解和抽象级别的问题,而不是测试它的问题。

找到类的契约在重构过程中发生更改也是合理的,例如,随着时间的推移,其抽象级别会提高。最初,它是关于特定临时位置中的两个特定文件的,但是随着时间的流逝,您可能会发现有必要进行其他抽象。此时,请更改测试以使其与班级合同保持同步。无需仅仅因为您正在测试就过度构建类的合同(YAGNI)。

当一个班级的合同没有很好地定义时,对它的测试可以使我们质疑班级的性质,但是使用它也可以。我想说,您不应该仅仅因为测试它就升级了该类的合同。您应出于其他原因升级该类的合同,例如,它是域的弱抽象,如果不是,则按原样对其进行测试。


4

@Erik提出的建议-确保您清楚要测试的内容-肯定是您考虑的第一点。

但是,您的决定是否应该使您朝着排除常量的方向发展,从而留下了问题的有趣部分(措辞):“为什么我应该权衡复制常量以复制代码?”。(参考您谈论“复制out_dir +名称步骤”的位置。)

我相信(对Erik的评论进行模运算)大多数情况确实会从删除重复的常量中受益。但是您需要以重复代码的方式来执行此操作。在您的特定示例中,这很容易。不要在生产代码中将路径当作“原始”字符串来处理,而是将路径视为路径。与字符串连接相比,这是连接路径组件的更可靠的方法:

os.path.join(self.out_dir, name)

另一方面,在您的测试代码中,我会建议类似的东西。在这里,重点显示您具有路径,并且正在“插入”叶文件名:

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

也就是说,通过更仔细地选择语言元素,您可以自动避免代码重复。(并非一直如此,但根据我的经验,很多时候都是如此。)


1

我同意Erik Eidt的回答,但是还有第三种选择:在测试中保留常量,因此即使您在生产代码中更改了常量的值,测试也通过了。

(请参阅在python unittest中存根常量

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

当执行这样的操作时,我通常会确保执行健全性测试,在不使用该with语句的情况下运行测试,并确保我看到“'a.foo_file'!='unique_to_your_test_a'”,然后将该with语句放回测试中所以它再次通过。

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.