编写单元测试时,我有时剪切并粘贴测试,不记得更改方法名称。这导致覆盖先前的测试,有效地隐藏它并阻止它运行。例如;
class WidgetTestCase(unittest.TestCase):
def test_foo_should_do_some_behavior(self):
self.assertEquals(42, self.widget.foo())
def test_foo_should_do_some_behavior(self):
self.widget.bar()
self.assertEquals(314, self.widget.foo())
在这种情况下,只会调用后一个测试。有没有一种以编程方式捕获此类错误的方法,而不是直接解析原始源代码?
答案 0 :(得分:23)
如果您对代码运行pylint,则会在您覆盖其他方法时通知您:
例如,我跑了这个:
class A(object):
def blah(self):
print("Hello World!")
def blah(self):
print("I give up!")
在this online pylint checker。除了所有遗失的文档字符串之外,我得到了这个:
E: 5:A.blah: method already defined line 2
答案 1 :(得分:14)
以下是使用未记录的,特定于实现的Python功能的可怕黑客攻击。你永远不应该曾经做这样的事情。
它已在Python 2.6.1和2.7.2上测试过;似乎没有使用Python 3.2编写,但是,无论如何,你可以在Python 3.x中do this right。
import sys
class NoDupNames(object):
def __init__(self):
self.namespaces = []
def __call__(self, frame, event, arg):
if event == "call":
if frame.f_code.co_flags == 66:
self.namespaces.append({})
elif event in ("line", "return") and self.namespaces:
for key in frame.f_locals.iterkeys():
if key in self.namespaces[-1]:
raise NameError("attribute '%s' already declared" % key)
self.namespaces[-1].update(frame.f_locals)
frame.f_locals.clear()
if event == "return":
frame.f_locals.update(self.namespaces.pop())
return self
def __enter__(self):
self.oldtrace = sys.gettrace()
sys.settrace(self)
def __exit__(self, type, value, traceback):
sys.settrace(self.oldtrace)
用法:
with NoDupNames():
class Foo(object):
num = None
num = 42
结果:
NameError: attribute 'num' already declared
工作原理:我们连接到系统跟踪钩子。每次Python即将执行一行时,我们都会被调用。这允许我们查看执行的 last 语句定义了哪些名称。为了确保我们能够捕获重复项,我们实际上维护了自己的局部变量字典,并在每行之后清除 Python。在类定义的最后,我们将本地复制回Python。其他一些tomfoolery用于处理嵌套类定义并在单个语句中处理多个赋值。
作为一个缺点,我们“清除所有当地人!”方法意味着你不能这样做:
with NoDupNames():
class Foo(object):
a = 6
b = 7
c = a * b
因为据Python所知,执行a
时没有名称b
和c = a * b
;我们看到他们后立即清理了那些。此外,如果您在一行中分配相同的变量两次(例如,a = 0; a = 1
),它将无法捕获。但是,它适用于更典型的类定义。
此外,除了类定义之外,您不应该在NoDupNames
上下文中添加任何内容。我不知道会发生什么;也许没什么不好。但是我没有尝试过,所以从理论上讲,宇宙可以被吸进自己的插孔中。
这很可能是我写过的最邪恶的代码,但它确实很有趣!
答案 2 :(得分:6)
以下是如何使用装饰器在运行时检测此问题的一个选项,而无需任何分析工具:
def one_def_only():
names = set()
def assert_first_def(func):
assert func.__name__ not in names, func.__name__ + ' defined twice'
names.add(func.__name__)
return func
return assert_first_def
class WidgetTestCase(unittest.TestCase):
assert_first_def = one_def_only()
@assert_first_def
def test_foo_should_do_some_behavior(self):
self.assertEquals(42, self.widget.foo())
@assert_first_def
def test_foo_should_do_some_behavior(self):
self.widget.bar()
self.assertEquals(314, self.widget.foo())
尝试导入或运行的示例:
>>> import testcases
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "testcases.py", line 13, in <module>
class WidgetTestCase(unittest.TestCase):
File "testcases.py", line 20, in WidgetTestCase
@assert_first_def
File "testcases.py", line 7, in assert_first_def
assert func.__name__ not in names, func.__name__ + ' defined twice'
AssertionError: test_foo_should_do_some_behavior defined twice
答案 3 :(得分:4)
您无法在运行时轻松/干净地检测到它,因为只需更换旧方法,并且必须在每个函数定义上使用装饰器。静态分析(pylint等)是最好的方法。
但是,你可能能够创建一个实现 - 刚测试它并且元类的__setattr__
的元类,并测试是否覆盖了一个方法。__setattr__
是没有调用类块中定义的东西。
答案 4 :(得分:1)
如果您有一个构建(例如 Jenkins CI/CD)在提出拉取请求后运行测试,您可以添加类似的内容
pylint --fail-under=7 --fail-on=
E0102
paths_of_files_changed
这意味着如果 Function defined already
E0102
OR 代码质量低于 7
,则返回非零退出代码。
然后可用于使构建失败。
或者,您可能有兴趣将其与 git pre commit hook 集成,通过它可以让您在提交时执行特定命令,如果它们失败,则不会引发您提交。
在结帐的根目录中创建一个名为 .pre-commit-config.yaml
与以下
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/PyCQA/pylint
rev: v2.9.6
hooks:
- id: pylint
args: [--fail-under=7, --fail-on=E0102]
(首选方法是)使用 pipx 或在虚拟环境之外执行此操作,因为它是 CLI 应用程序
python3 -m pip install pre-commit
pre-commit install # you only do this once per "git clone"
在所有函数仅定义一次之前,您无法提交。 这两种解决方案都将阻止任何被多次定义的方法提交到代码库中。