15

在最近的代码审查一个,我在那没有立即容易发现问题绊倒 - 有用来代替assertEqual()assertTrue(),基本上导致成一个测试,是测试没有。这里是一个简化的例子:检测不正确的断言方法

from unittest import TestCase 


class MyTestCase(TestCase): 
    def test_two_things_equal(self): 
     self.assertTrue("a", "b") 

这里的问题是测试会通过;从技术上讲,代码是有效的,因为assertTrue has this optional msg argument(在这种情况下获得"b"值)。

我们可以做得比依靠审查代码的人发现这类问题更好吗?有没有办法自动检测它使用静态代码分析与flake8pylint

+1

但萎靡不振的技术上有效的代码可能会创造一个全新的不同的问题; *信息溢出*。 –

+0

@ Ev.Kounis对,如果我们天真地接近它,可能会有很多误报。我正在考虑只强制使用'msg'作为关键字参数。在这种情况下,我们可能会通过警告消息未正确传递给断言方法来捕获此特定问题。我不太喜欢这个想法,但希望看看有没有其他的想法..谢谢。 – alecxe

+1

我强烈建议@Leon提供的答案。通过添加你知道应该失败的测试,你会发现一个被滥用的测试用例。 –

回答

5

几年前,我想出了一种通用方法/方法来确保测试质量。测试的规范可以减少到两个子句:

  1. 它必须正确执行该功能的被测试,并且
  2. 它必须失败不正确/断实施被测试的功能

据我所知,虽然要求1.经常被执行,但很少关注p援助要求2

通常

  • 一个测试套件被创建,
  • 代码对其运行,
  • 任何失败(因为无论是在代码或在测试中的错误的)是固定的
  • 当我们相信我们的代码和测试是好的时,我们就会遇到这种情况。

实际情况可能是(某些)测试包含(会)阻止它们捕获代码中的错误的错误。因此,看到测试通过不应该提示关心系统质量的人很安静,直到他们确信测试确实能够检测到他们针对设计的问题。而一个简单的方法就是实际引入这些问题并检查它们是否不被测试所忽视!

在TDD(测试驱动开发)中,这个想法只被部分地遵循 - 建议在代码之前添加测试,看看它失败(应该,因为没有代码),然后修复它编写代码。但是由于缺少代码而导致的测试失败并不意味着它在错误代码的情况下也会失败(这对您的情况似乎是正确的)!

因此,测试套件的质量可以衡量为它能够检测到的错误的百分比。任何合理的转义测试套件的错误提示涵盖该场景的新测试用例(或者,如果测试套件应该已经捕获该错误,测试套件中的错误被发现)。这也意味着套件的每个测试必须能够捕获至少一个错误(否则,该测试完全没有意义)。

我在考虑实施一个有助于采用这种方法的软件系统(即允许在代码库中注入和维护人工错误并检查测试如何响应它们)。这个问题成为我即将开始研究的触发器。希望能在一周内把东西放在一起。敬请关注!

编辑

工具的原型版本现已在https://bitbucket.org/leon_manukyan/trit。我建议克隆存储库并运行演示流程。


此语句的更一般化版本为更广泛的系统/情况真(所有通常具有安全做/安全):

一种系统设计对某些必须定期对这些事件进行事件测试,否则它很容易降解到完全无法对感兴趣的事件作出反应。

只是一个例子 - 你家有火警系统吗?你上次什么时候见证过它的工作?如果在火灾中保持沉默,该怎么办?现在就在房间里抽一些烟吧!

在这种方法的范围,像臭虫后门(例如,当功能行为不端只有的在URL中传递等于https://www.formatmyharddrive.com/?confirm=yesofcourse)是不是一个合理的

+0

你说得对,很棒。严格地说,我们应该总是看到一个测试失败“故意” - 确保我们正在测试我们的目标。谢谢! – alecxe

+0

@alecxe该工具的原型版本已准备就绪。在https://bitbucket.org/leon_manukyan/trit找到它。我提供了一个演示流程,其问题类似于您的问题。 – Leon

+0

'trit'是关于我一直在想的很长一段时间,这太棒了!我一定会回顾一下,看看我是否可以将它应用到我的日常工作流程中。非常感谢! – alecxe

10

Python现在有一个类型提示系统,可以进行静态代码分析。使用这个系统,你可以要求像assertTrue这样的函数的第一个参数总是布尔值。问题是assertTrue不是由你定义的,而是由unittest包定义的。不幸的是,unittest包没有添加类型提示。尽管有一个相当简单的方法:只需定义你自己的包装。

from unittest import TestCase 

class TestCaseWrapper(TestCase): 
    def assertTrue(self, expr: bool, msg=None): #The ": bool" requires that the expr parameter is boolean. 
     TestCase.assertTrue(self, expr, msg) 

class MyTestCase(TestCaseWrapper): 
    def test_two_things_equal(self): 
     self.assertTrue("a", "b") #Would give a warning about the type of "a". 

然后,您可以运行类型检查就像这样:

python -m mypy my_test_case.py 

这应该然后给你一个关于如何报警“a”是一个字符串,而不是一个布尔值。关于这一点的好处是它可以在自动化测试框架中自动运行。此外,PyCharm会检查代码中的类型,如果您提供它们并突出显示任何错误的内容。

+3

当制作这样的包装时,使用'functools.wraps'来制作文档以及从原始函数继承它也是有用的。 – Ghostkeeper

+0

建议第一种类型是布尔值是一个可怕的想法,因为您可能只是检查值是否真实。 –

2

快速解决方案将是,以提供检查正确性一个混合:

import unittest 


class Mixin(object): 
    def assertTrue(self, *args, **kwargs): 
     if len(args) > 1: 
      # TypeError is just an example, it could also do some warning/logging 
      # stuff in here. 
      raise TypeError('msg should be given as keyword parameter.') 
     super().assertTrue(*args, **kwargs) 


class TestMixin(Mixin, unittest.TestCase): # Mixin before other parent classes 
    def test_two_things_equal(self): 
     self.assertTrue("a", "b") 

的密新还可以检查是否传递表达是一个布尔:

class Mixin(object): 
    def assertTrue(self, *args, **kwargs): 
     if type(args[0]) is bool: 
      raise TypeError('expression should be a boolean') 
     if len(args) > 1: 
      raise TypeError('msg should be given as keyword parameter.') 
     super().assertTrue(*args, **kwargs) 

但是,这不是静态的,它需要手动更改测试类(添加Mixin)并运行测试。此外,它会抛出大量的误报,因为将消息作为关键字参数传递并不常见(至少不是我见过的),并且在很多情况下,您想要检查表达式的隐含真实性而不是显式的bool。要检查不-空虚if aalistdict

你也可以使用一些setUpteardown代码,改变了特定类的assertTrue方法:

import unittest 


def decorator(func): 
    def wrapper(*args, **kwargs): 
     if len(args) > 1: 
      raise TypeError() 
     return func(*args, **kwargs) 
    return wrapper 


class TestMixin(unittest.TestCase): 
    def setUp(self): 
     self._old = self.assertTrue 
     self.assertTrue = decorator(self.assertTrue) 

    def tearDown(self): 
     self.assertTrue = self._old 

    def test_two_things_equal(self): 
     self.assertTrue("a", "b") 

但谨慎使用这些方法之前的注意事项:在改变现有测试之前,务必小心谨慎。不幸的是,测试的记录有时很差,所以测试和测试的方式并不总是很明显。某些时候测试没有意义,并且可以改变它,但有时它会以奇怪的方式测试某个特定功能,当您更改测试时会改变正在测试的内容。因此,至少要确保在更改测试用例时不会发生覆盖范围变化。如有必要,请确保通过更新方法名称,方法文档或内嵌评论来阐明测试的目的。

2

这种问题的一个解决方案是使用"mutation testing"。这个想法是通过在代码中引入小的变化来自动生成代码的“突变体”。然后,您的测试套件针对这些突变体运行,如果它们是好的,则大多数应该被杀死,这意味着您的测试套件检测到突变并且测试失败。

突变测试实际评估您的测试质量。在你的例子中,没有突变体会被杀死,你会很容易发现测试有问题。

在Python中,有几种突变框架可供选择: