Python中的TDD是否被破坏?
假设我们有一个叫做 UserService
的类,它里面有一个属性叫 current_user
。这个类在 AppService
类中被使用。
我们已经为 AppService
写好了测试。在测试准备阶段,我们用一个假值来替代 current_user
:
UserService.current_user = 'TestUser'
假设我们决定把 current_user
改名为 active_user
。我们在 UserService
中改了名字,但忘了在 AppService
中也做相应的修改。
我们运行测试,结果测试通过了!因为测试准备阶段添加了 current_user
这个属性,而它在 AppService
中仍然(虽然是错误的,但成功地)被使用。
现在我们的测试就没用了。虽然测试通过了,但在实际运行中,应用会出错。
我们不能依赖我们的测试套件,这意味着测试驱动开发(TDD)就不可能了。
那么在 Python 中,TDD 是不是就坏掉了呢?
3 个回答
在这个变化之前,对象的行为应该会根据当前用户的值有所不同。我们把这个不同的地方叫做predicate()。抱歉我用的是Python,下面是伪代码:
UserService.current_user = 'X'
assertFalse(obj.predicate())
UserService.current_user = 'Y'
assertTrue(obj.predicate())
明白了吗?这就是你的测试。让它通过。现在把你正在测试的类里的current_user改名为active_user。这样一来,测试就会失败,要么在第一个检查的时候,要么在第二个检查的时候。因为你不再改变之前叫做current_user的那个字段的值,所以在这两种情况下,predicate的结果都会是假的或者真的。现在你有了一个非常专注的测试,它会在类的变化使得其他测试的设置失效时提醒你。
问题其实不在于测试驱动开发(TDD)或者Python。首先,TDD并不能证明当你所有的测试都通过时,你的应用程序就是好的。比如说,想象一个叫做multiplyBy2()的函数,你用1、2、3作为输入,期望得到2、4、6的输出,但如果你把multiplyBy2写成了平方运算,那所有的测试都通过了,你的代码覆盖率也达到了100%,但你的实现却是错误的。你需要明白,TDD只能告诉你,当你的测试失败时,说明你的应用程序有问题,仅此而已。所以,正如其他回答所说,问题在于你没有一个会失败的测试。如果你使用的是一些静态类型的语言,编译器会帮你检查这个问题,并会对你使用不存在的方法发出警告。这并不是说你必须使用静态类型的语言,只是说在动态类型的语言中,你需要写更多的测试。如果你想确保代码的正确性,可以考虑使用契约设计来确保至少在运行时的正确性,以及使用正式规范来为某些算法提供证明,但这可能离标准编码还有一段距离。
好的,我找到了解决办法。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
下面是如何用不同的用户来测试 AppService
的 can_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
,那么在尝试修改这个对象时会出现错误。这正是我想要的效果。