Python中的TDD是否被破坏?

3 投票
3 回答
541 浏览
提问于 2025-04-15 22:11

假设我们有一个叫做 UserService 的类,它里面有一个属性叫 current_user。这个类在 AppService 类中被使用。

我们已经为 AppService 写好了测试。在测试准备阶段,我们用一个假值来替代 current_user

UserService.current_user = 'TestUser'

假设我们决定把 current_user 改名为 active_user。我们在 UserService 中改了名字,但忘了在 AppService 中也做相应的修改。

我们运行测试,结果测试通过了!因为测试准备阶段添加了 current_user 这个属性,而它在 AppService 中仍然(虽然是错误的,但成功地)被使用。

现在我们的测试就没用了。虽然测试通过了,但在实际运行中,应用会出错。

我们不能依赖我们的测试套件,这意味着测试驱动开发(TDD)就不可能了。

那么在 Python 中,TDD 是不是就坏掉了呢?

3 个回答

0

在这个变化之前,对象的行为应该会根据当前用户的值有所不同。我们把这个不同的地方叫做predicate()。抱歉我用的是Python,下面是伪代码:

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

明白了吗?这就是你的测试。让它通过。现在把你正在测试的类里的current_user改名为active_user。这样一来,测试就会失败,要么在第一个检查的时候,要么在第二个检查的时候。因为你不再改变之前叫做current_user的那个字段的值,所以在这两种情况下,predicate的结果都会是假的或者真的。现在你有了一个非常专注的测试,它会在类的变化使得其他测试的设置失效时提醒你。

2

问题其实不在于测试驱动开发(TDD)或者Python。首先,TDD并不能证明当你所有的测试都通过时,你的应用程序就是好的。比如说,想象一个叫做multiplyBy2()的函数,你用1、2、3作为输入,期望得到2、4、6的输出,但如果你把multiplyBy2写成了平方运算,那所有的测试都通过了,你的代码覆盖率也达到了100%,但你的实现却是错误的。你需要明白,TDD只能告诉你,当你的测试失败时,说明你的应用程序有问题,仅此而已。所以,正如其他回答所说,问题在于你没有一个会失败的测试。如果你使用的是一些静态类型的语言,编译器会帮你检查这个问题,并会对你使用不存在的方法发出警告。这并不是说你必须使用静态类型的语言,只是说在动态类型的语言中,你需要写更多的测试。如果你想确保代码的正确性,可以考虑使用契约设计来确保至少在运行时的正确性,以及使用正式规范来为某些算法提供证明,但这可能离标准编码还有一段距离。

1

好的,我找到了解决办法。Python的库 Mock 正好满足我的需求

下面是我最终写出来的代码。

模型和服务的定义:

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,那么在尝试修改这个对象时会出现错误。这正是我想要的效果。

撰写回答