Python单元测试-与assertRaises相反吗?


374

我想编写一个测试来确定在给定的情况下不引发异常。

测试是否引发异常很简单 ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

...但是你怎么做相反

像这样的东西我在追求...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

6
您总是可以简单地做应该在测试中工作的任何事情。如果引发错误,则会显示出来(算作错误,而不是失败)。当然,这假定它不会引发任何错误,而不仅仅是引发已定义的错误类型。除此之外,我想您必须自己编写。
Thomas K


事实证明,实际上您可以实现一种assertNotRaises方法,该方法assertRaises在大约30行左右的代码行中共享其代码/行为的90%。请参阅下面的详细信息。
电话

我想要这样做,因此我可以比较两个函数,hypothesis以确保它们对于各种输入产生相同的输出,而忽略原始函数引发异常的情况。 assume(func(a))不起作用,因为输出可以是真值含糊的数组。所以我只想调用一个函数并获取True它是否不会失败。 assume(func(a) is not None)我猜的作品
endolith '19

Answers:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon-不,实际上这是正确的解决方案。由user9876提出的解决方案在概念上有缺陷:如果你测试非募集发言权ValueError,但ValueError将改为引发,您的测试必须以失败条件,而不是错误一个退出。另一方面,如果在运行相同的代码时将引发KeyError,那将是错误,而不是失败。在python中-与某些其他语言不同-异常通常用于控制流,这就是我们except <ExceptionName>确实具有语法的原因。就此而言,user9876的解决方案是完全错误的。
mac

@mac-这也是正确的解决方案吗?stackoverflow.com/a/4711722/6648326
MasterJoe2年

不幸的是,该结果显示测试的<100%覆盖率(除非永远不会发生)。
Shay

3
@ Shay,IMO,您应该始终将测试文件本身从覆盖率报告中排除(由于按定义它们几乎总是运行100%,您将人为地夸大报告)
原始的烧烤酱

@ original-bbq-sauce,这不会让我无意间跳过测试。例如,测试名称(ttst_function)中的拼写错误,pycharm中的运行配置错误等?
谢伊(Shay)

67

嗨-我想编写一个测试来确定在给定的情况下不引发异常。

这是默认的假设-不引发异常。

如果您什么都没说,则在每个测试中都假设了这一点。

您不必为此写任何断言。


7
@IndradhanushGupta公认的答案使测试比此测试更具Python性。显式胜于隐式。
2015年

17
没有其他评论者指出为什么这个答案是错误的,尽管与user9876的答案是错误的原因相同:失败和错误是测试代码中的不同事物。如果你的函数一个测试,没有断言期间抛出一个异常,测试框架将可以把它看成一个错误,而不是一个失败不能断言。
coredumperror

@CoreDumpError我理解失败和错误之间的区别,但是这是否会迫使您用try / exception结构包围每个测试?还是建议您只对在某些情况下显式引发异常的测试(基本上意味着该异常是预期的)执行此操作。
federicojasson

4
@federicojasson您在第二句话中很好地回答了自己的问题。测试中的错误与失败可以分别简单地描述为“意外崩溃”与“意外行为”。您希望您的测试在函数崩溃时显示错误,而不是在给定不同输入的情况下抛出您知道会在给定特定输入的情况下抛出的异常。
coredumperror

52

只需调用该函数即可。如果引发异常,则单元测试框架会将其标记为错误。您可能要添加评论,例如:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

35
失败和错误在概念上是不同的。此外,由于在python中通常将异常用于控制流,因此如果您破坏了逻辑或代码,一眼就很难理解(=不探索测试代码)...
mac

1
您的测试通过或未通过。如果未通过,则必须修复它。被报告为“故障”还是“错误”几乎无关紧要。有一个区别:通过我的回答,您将看到堆栈跟踪,因此您可以看到PathIsNotAValidOne的抛出位置;有了可接受的答案,您将不会获得该信息,因此调试将更加困难。(假设Py2;不确定Py3是否在此方面更好)。
user9876 2014年

19
@ user9876-不。测试退出条件为3(通过/不通过/错误),而不是您似乎错误相信的2。错误和失败之间的区别是巨大的,并且将它们视为相同只是编程不良。如果您不相信我,请环顾四周,看看测试运行者的工作方式以及他们实施失败和错误的决策树。python的一个很好的起点是pytest中xfail装饰器
mac

4
我想这取决于您如何使用单元测试。我的团队使用单元测试的方式必须全部通过。(使用连续的集成机器运行所有单元测试的敏捷编程)。我知道测试用例可以报告“通过”,“失败”或“错误”。但是从总体上来说,对我的团队而言真正重要的是“所有单元测试都通过了吗?” (即“詹金斯(Jenkins)是绿色的吗?”)。因此,对于我的团队而言,“失败”和“错误”之间没有实际区别。如果以不同的方式使用单元测试,则可能会有不同的要求。
user9876 '16

1
@ user9876“ fail”和“ error”之间的区别是“我的断言失败”和“我的测试甚至没有达到断言”之间的区别。对我来说,这是在修复测试过程中有用的区别,但是正如您所说,我想并不是每个人都可以。
CS CS

14

我是原始发布者,我没有在代码中首先使用它就接受了DGH的上述回答。

一旦我使用完,我意识到实际上需要做一些调整才能做我需要做的事情(为了公平对待DGH,他/她确实说了“或类似的话!”)。

我认为值得在这里发表一些调整以使他人受益:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

我在这里尝试做的是确保如果尝试使用第二个空格参数实例化Application对象,则将引发pySourceAidExceptions.PathIsNotAValidOne。

我相信使用上面的代码(主要基于DGH的答案)可以做到。


2
由于您是在澄清问题而不是回答问题,因此您应该对其进行编辑(未回答)。请在下面查看我的回答。
hiwaylon

13
这似乎与原始问题完全相反。self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)在这种情况下应该做的工作。
Antony Hatchkins

8

您可以定义assertNotRaises通过重用对原执行的90%assertRaises的中unittest模块。使用这种方法,最终得到的assertNotRaises方法除了其反向失败条件外,其行为与相同assertRaises

TLDR和现场演示

事实证明,向其中添加assertNotRaises方法非常容易unittest.TestCase(编写此答案所花的时间是代码的四倍,这使我花了大约四倍的时间)。这是该assertNotRaises方法的现场演示。就像一样assertRaises,您可以将callable和args传递给assertNotRaises,也可以在with语句中使用它。现场演示包括一个测试案例,演示了assertNotRaises预期的工作方式。

细节

assertRaisesin 的实现unittest相当复杂,但是通过一些巧妙的子类化,您可以覆盖和逆转其失败条件。

assertRaises是一个简短的方法,基本上只创建unittest.case._AssertRaisesContext类的实例并返回它(请参见unittest.case模块中的定义)。您可以_AssertNotRaisesContext通过继承_AssertRaisesContext并覆盖其__exit__方法来定义自己的类:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

通常,您可以通过从中继承来定义测试用例类TestCase。如果您改为继承子类MyTestCase

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

现在,您所有的测试用例都将具有assertNotRaises可用的方法。


traceback的对else帐单来自何处?
NOhs '18 -10-26

1
@NOhs丢失了import。它的固定位置
电话

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

如果您需要接受参数,可以进行修改。

self._assertNotRaises(IndexError, array, 'sort')

1

我发现unittest按以下步骤进行猴子补丁很有用:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

这在测试是否存在异常时阐明了意图:

self.assertMayRaise(None, does_not_raise)

这也简化了循环测试,我经常发现自己在做:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

什么是猴子补丁?
ScottMcC

1
请参阅en.wikipedia.org/wiki/Monkey_patch。添加后assertMayRaiseunittest.TestSuite您可以假装它是unittest库的一部分。
AndyJost

0

如果将Exception类传递给assertRaises(),则将提供一个上下文管理器。这可以提高测试的可读性:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

这使您可以在代码中测试错误情况。

在这种情况下,您正在测试PathIsNotAValidOne当您将无效参数传递给Application构造函数时引发的。


1
不,只有在上下文管理器块中引发异常的情况下,此操作才会失败。可以很容易地通过“ with self.assertRaises(TypeError):提高TypeError”进行测试。
马修·特雷弗

@MatthewTrevor好的电话。我记得,不是建议测试代码正确执行(即没有提高),而是建议测试错误情况。我已经相应地编辑了答案。希望我能赚+1以摆脱困境。:)
hiwaylon 2012年

请注意,这也是Python 2.7版及更高版本:docs.python.org/2/library/...
qneill

0

你可以这样尝试。尝试:self.assertRaises(None,function,arg1,arg2)除外:如果不将代码放入try块,则通过,它将通过异常“ AssertionError:未引发”,测试用例将失败。测试用例将通过如果放在try块中,这是预期的行为。


0

确保对象初始化没有任何错误的一种简单方法是测试对象的类型实例。

这是一个例子:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.