【问题标题】:Is TDD broken in Python?TDD 在 Python 中被破坏了吗?
【发布时间】:2011-02-14 01:22:12
【问题描述】:

假设我们有一个类UserService,其属性为current_user。假设在AppService类中使用。

我们为AppService 提供了测试。在测试设置中,我们使用一些模拟值将 current_user 存根:

UserService.current_user = 'TestUser'

假设我们决定将current_user 重命名为active_user。我们将其重命名为 UserService,但忘记在 AppService 中更改其用法。

我们运行测试并且它们通过了!测试设置添加了current_user 属性,它仍然(错误但成功地)在AppService 中使用。

现在我们的测试没用了。他们通过了,但应用程序将在生产中失败。

我们不能依赖我们的测试套件 ==> TDD 是不可能的。

TDD 在 Python 中被破坏了吗?

【问题讨论】:

  • 您无法在 Python 中进行 TDD!= Python 中的 TDD 已损坏。
  • 这是你设置你的模拟值的方式被破坏了,你应该使用普通的 UserService 构造函数来设置 current_user (或实际应用程序中的任何更改),而不是使用这个快捷方式。
  • @Pierreten,最好告诉我我做错了什么
  • @Luper Rouch,UserService 构造函数不能接受用户,current_user 实际上是一个属性 getter,它从环境中检索当前用户(例如,当前 http 上下文)
  • 然后写UserService.current_user = "TestUser" 用字符串覆盖property,您的测试不再反映生产条件。正确的方法是修改属性返回的内容,例如通过设置它通常返回的环境变量。

标签: python unit-testing refactoring tdd mocking


【解决方案1】:

问题不在于 TDD,也不在于 Python。首先,TDD 并不能证明当您的所有测试都通过时,您的应用程序就是好的。想象一下,例如multiplyBy2() 函数,可以使用输入 1,2,3 和输出 2,4,8 进行测试,现在想象一下,您将 multiplyBy2 实现为平方。你所有的测试都通过了,你有 100% 的代码覆盖率并且你的实现是错误的。你必须明白,TDD 只能给你保证,一旦你的测试失败,你的应用程序就有问题,仅此而已。因此,正如其他答案中所建议的那样,问题在于您没有失败的测试。如果您使用了某种静态类型语言,编译器会为您进行该测试并抱怨使用不存在的方法。这并不意味着你应该使用静态类型的语言,它只是意味着你需要用动态类型的语言编写更多的测试。 如果您对强制执行代码的正确性感兴趣,您应该通过合同来查看设计,以确保至少在运行时和正式规范中的正确性,以至少为某些算法提供证明,但我想这与标准编码相去甚远。

【讨论】:

  • TDD 还让我确信我的旧功能仍然存在(也就是没有回归)。 DBC 听起来像是一个可能的解决方案
  • @Konstantin 不,它没有,你没有理解我的意思。以我的乘法示例为例,假设它以正确的方式实现,您的测试通过,您进行更改 - 用平方代替乘法,您的测试仍然通过,但您可能引入了回归错误。真的只有失败的测试才能给你一些真实的信息。我非常喜欢 TDD,但我认为真正了解它的工作原理和作用非常重要。
  • 好的,我的问题是如何编写测试我的生产代码的测试,而不是测试自己的测试。我不能同意,如果测试没有失败,它不会给你任何信息。每个测试都包含有关系统的知识,如果您愿意,可以保持不变。不变量不应该改变,如果在重构测试通过之后,这意味着你之前拥有的功能现在仍然拥有。如果 test_multiplyBy2 即使在使用 square 时也没有失败,那么测试很糟糕。我正在寻找一种方法来编写一个好的测试。
  • @Gabriel - 3^2 != 8; 1^2 != 2。您的观点当然是正确的,但是您的示例可以进行一些修复。 0^2 == 0 * 2; 2^2 == 2 * 2。
  • @Konstantin 对不起,我不想听起来好像什么都没说,它只是给你没有证据。它为您提供信息,但仅针对这种情况,这就是我想要暗示的事情。具有不变量的事情是它与限制性一样好,例如总是返回 true 的测试是一个测试,但没有任何意义。正如你最后所说 - 问题是编写好的测试。
【解决方案2】:

好的,我找到了解决方案。 Python库Mockdoes what I want

下面是我最终得到的代码。

模型和服务定义:

class User(object):
    def __init__(self):
        self.roles = []


class UserService(object):
    def get_current_user(self):
        return None # get from environment, database, etc.

    current_user = property(get_current_user)


class AppService(object):
    def __init__(self, userService):
        self.userService = userService

    def can_write(self):
        return 'admin' in self.userService.current_user.roles

下面是如何用不同的用户测试AppServicecan_write方法:

class AppServiceTests(unittest.TestCase):
    def test_can_write(self):
        user = User()

        @patch_object(UserService, 'current_user', user)
        def can_write():
            appService = AppService(UserService())
            return appService.can_write()

        user.roles = ['admin']
        self.assertTrue(can_write())

        user.roles = ['user']
        self.assertFalse(can_write())

如果您仅在类UserService 中重命名属性current_user,则在尝试修补对象时会出错。这是我一直在寻找的行为。

【讨论】:

    【解决方案3】:

    在更改之前,对象的行为应该有所不同,具体取决于 current_user 的值。让我们称之为谓词()。原谅我的蟒蛇;考虑这个伪代码:

    UserService.current_user = 'X'
    assertFalse(obj.predicate())
    UserService.current_user = 'Y'
    assertTrue(obj.predicate())
    

    好吗?所以,这是你的测试。让它通过。现在更改正在测试的类,以便将 current_user 重命名为 active_user。现在测试将失败,无论是在第一个断言处,还是在第二个断言处。因为您不再更改以前称为 current_user 的字段的值,所以谓词在这两种情况下都将为 false 或 true。现在您有了一个非常集中的测试,当类发生更改时会提醒您,从而使其他测试的设置无效。

    【讨论】:

    • 也许我不明白predicate 应该是什么样子。如果predicate 正在使用current_user,则在我将UserService 的属性重命名为active_user 后,此测试不会失败,因为此测试自己设置了current_user
    • 您现有的测试依赖于该设置 - 它对 active_user / current_user 拆分不敏感。因此,您需要一个测试,也就是说,如果您希望自动检测到此类更改。作为 unit 测试,您希望测试一些小东西 - 您不希望现有测试同时测试它正在测试的内容,字段的名称。所以添加一个只对字段名称敏感的新测试。
    • 检查字段名称的理想测试将是assert(hasattr(UserService, 'current_user'))。我重命名为active_user 后,此测试将失败,我会修复它。但是我会忘记第二个测试,它会一直是绿色的。
    猜你喜欢
    • 1970-01-01
    • 2021-05-17
    相关资源
    最近更新 更多