23

在编写单元测试时,我有时会剪切和粘贴测试并且不记得更改方法名称。这会导致覆盖之前的测试,有效地隐藏它并阻止它运行。例如;

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())

在这种情况下,只有后一个测试会被调用。有没有办法以编程方式捕获这种错误,而不是直接解析原始源代码?

4

5 回答 5

27

如果您在代码上运行pylint,它会在您覆盖另一个方法时通知您:

例如,我运行了这个:

class A(object):
    def blah(self):
        print("Hello, World!")

    def blah(self):
        print("I give up!")

这个在线 pylint 检查器中。除了所有缺少的文档字符串等,我得到了这个:

E: 5:A.blah: method already defined line 2

或者,通过命令行:

$ python -m pyflakes .
.\blah.py:5:5 redefinition of unused 'blah' from line 2
于 2012-05-25T22:25:11.213 回答
15

接下来是一个可怕的 hack,它使用了未记录的、特定于实现的 Python 特性。你永远不应该这样的事情。

它已经在 Python 2.6.1 和 2.7.2 上进行了测试;似乎不适用于编写的 Python 3.2,但是无论如何,您都可以在 Python 3.x 中正确执行此操作。

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 即将执行一行时,我们都会被调用。这使我们可以看到最后执行的语句定义了哪些名称。为了确保我们可以捕获重复项,我们实际上维护了自己的局部变量字典并在每行之后清除Python 的。在类定义的最后,我们将本地变量复制回 Python 中。其他一些愚蠢的东西在那里处理嵌套类定义并在单个语句中处理多个分配。

不利的一面是,我们的“清除所有当地人!” 方法意味着你不能这样做:

with NoDupNames():
    class Foo(object):
        a = 6
        b = 7
        c = a * b

因为据 Python 所知,没有名称ab执行时间c = a * b;我们一看到它们就清除了它们。此外,如果您在一行中两次分配相同的变量(例如,a = 0; a = 1),它不会捕捉到这一点。但是,它适用于更典型的类定义。

此外,除了类定义之外,您不应将任何内容放在NoDupNames上下文中。我不知道会发生什么;也许没什么不好。但我还没有尝试过,所以理论上宇宙可以被吸入它自己的塞孔。

这很可能是我写过的最邪恶的代码,但它确实很有趣!

于 2012-05-26T01:23:53.343 回答
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
于 2012-05-25T22:36:50.240 回答
4

您不能在运行时轻松/干净地检测到它,因为旧方法被简单地替换并且必须在每个函数定义上使用装饰器。静态分析(Pylint 等)是最好的方法。

我刚刚对其进行了测试,并且__setattr__没有为类块中定义的内容调用元类。

于 2012-05-25T22:24:57.753 回答
1
通过 CI/CD 解决方案

如果您有一个构建(例如 Jenkins CI/CD),一旦提出拉取请求就会运行测试,您可以添加类似 pylint --fail-under=7 --fail-on=E0102 paths_of_files_changed

这意味着如果Function defined alreadyE0102 OR 代码的质量小于7返回非零退出代码。

然后可以使用它来使构建失败。

通过预提交的客户端解决方案

或者,您可能有兴趣将其与 git pre commit 挂钩集成,通过该挂钩将允许您在提交时执行特定命令,如果它们失败,则不允许您提交。

在结帐的根目录中创建一个名为.pre-commit-config.yaml With 以下的文件

# 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"

在所有函数只定义一次之前,您无法提交。这两种解决方案都将阻止任何被多次定义的方法提交到代码库中。

于 2021-08-03T18:47:01.880 回答