Python unittest - 与assertRaises相反?

Posted

技术标签:

【中文标题】Python unittest - 与assertRaises相反?【英文标题】:Python unittest - opposite of assertRaises? 【发布时间】:2011-05-18 05:11:55 【问题描述】:

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

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

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

...但是你怎么能做到相反的

我追求的是这样的……

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

【问题讨论】:

你总是可以简单地做任何应该在测试中起作用的事情。如果它引发错误,则会显示出来(算作错误,而不是失败)。当然,这假设它不会引发任何错误,而不仅仅是定义类型的错误。除此之外,我想你必须自己写。 Python - test that succeeds when exception is not raised 的可能重复项 事实证明,您实际上可以实现一个assertNotRaises 方法,该方法与assertRaises 共享其90% 的代码/行为,大约需要大约30 行代码。详情请见my answer below。 我想要这个,所以我可以将两个函数与hypothesis 进行比较,以确保它们为各种输入产生相同的输出,同时忽略原始引发异常的情况。 assume(func(a)) 不起作用,因为输出可能是一个真值不明确的数组。所以我只想调用一个函数并得到True,如果它没有失败。 assume(func(a) is not None) 我猜是有效的 这能回答你的问题吗? Pass a Python unittest if an exception isn't raised 【参考方案1】:

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

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

已编辑以添加来自 cmets 的说明:

单元测试可以有 3 个结果:通过、失败、错误。 (如果算上 XPass/XFail/Skip,实际上更多...) 如果你在测试一个特定的异常没有被抛出,而它被抛出了,那么理论上应该是一个Fail。但是上面的代码使它成为一个错误,这在理论上是“错误的”。 实际上,如果出现错误,您的测试运行程序可能会打印堆栈跟踪,这可能有助于调试故障。如果出现 Fail,您可能不会看到堆栈跟踪。 实际上,使用 Fail 您可以将测试标记为“预期失败”。如果出现错误,您可能无法这样做,尽管您可以将测试标记为“跳过”。 实际上,使测试用例报告错误需要额外的代码。 “错误”和“失败”之间的区别是否重要取决于您的流程。我的团队使用单元测试的方式,他们必须全部通过。 (敏捷编程,使用运行所有单元测试的持续集成机器)。对我的团队来说真正重要的是“所有的单元测试都通过了吗?” (即“詹金斯是绿色的吗?”)。所以对我的团队来说,“失败”和“错误”之间没有实际区别。 由于上述优点(更少的代码,查看堆栈跟踪),以及我的团队对 Fail/Error 的处理方式相同的事实,我使用了这种方法。 如果您以不同的方式使用单元测试,您可能会有不同的要求,特别是如果您的流程以不同的方式处理“失败”和“错误”,或者如果您希望能够将测试标记为“预期失败”。 如果您希望此测试报告错误,请使用 DGH 的答案。

【讨论】:

失败和错误在概念上是不同的。此外,由于在 python 中,异常通常用于控制流,如果您破坏了逻辑或代码,这将非常难以一目了然(=不探索测试代码)...... 您的测试要么通过,要么不通过。如果它没有通过,你将不得不去修复它。报告为“失败”还是“错误”大多无关紧要。有一个区别:通过我的回答,您将看到堆栈跟踪,因此您可以看到 PathIsNotAValidOne 被抛出的位置;使用接受的答案,您将没有该信息,因此调试将更加困难。 (假设 Py2;不确定 Py3 在这方面是否更好)。 @user9876 - 不。测试退出条件是 3(通过/未通过/错误),而不是您似乎错误地认为的 2。错误和失败之间的区别是巨大的,将它们视为相同只是糟糕的编程。如果你不相信我,看看测试运行器是如何工作的,以及他们针对失败和错误实施的决策树。 python 的一个很好的起点是 pytest 中的the xfail decorator。 我想这取决于你如何使用单元测试。我的团队使用单元测试的方式,他们必须全部通过。 (敏捷编程,使用运行所有单元测试的持续集成机器)。我知道测试用例可以报告“通过”、“失败”或“错误”。但在高层次上,对我的团队来说真正重要的是“所有的单元测试都通过了吗?” (即“詹金斯是绿色的吗?”)。所以对我的团队来说,“失败”和“错误”之间没有实际区别。如果您以不同的方式使用单元测试,您可能会有不同的要求。 @user9876 “失败”和“错误”之间的区别是“我的断言失败”和“我的测试甚至没有到达断言”之间的区别。对我来说,在修复测试期间这是一个有用的区别,但我想,正如你所说,并不适合所有人。【参考方案2】:
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

【讨论】:

@hiwaylon - 不,这实际上是正确的解决方案。 user9876 提出的解决方案在概念上存在缺陷:如果您测试说 ValueError 的非提升,但改为提升 ValueError,则您的测试必须以失败条件退出,而不是错误条件。另一方面,如果在运行相同的代码时会引发KeyError,那将是一个错误,而不是失败。在 python 中 - 与其他一些语言不同 - 异常通常用于控制流,这就是我们确实有 except <ExceptionName> 语法的原因。对此,user9876 的解决方案是完全错误的。 这有一个不幸的效果,显示 @Shay,IMO,您应该始终将测试文件本身从覆盖率报告中排除(因为根据定义,它们几乎总是 100% 运行,您会人为地夸大报告) @original-bbq-sauce,这不会让我对无意跳过的测试持开放态度。例如,测试名称(ttst_function)中的拼写错误,pycharm 中的错误运行配置等? 要求对测试文件捕获 100% 的覆盖率对我来说已经发现了很多错误,否则这些错误可能不会被发现。【参考方案3】:

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

这是默认假设——不会引发异常。

如果你什么都不说,这是在每一个测试中假设的。

您实际上不必为此编写任何断言。

【讨论】:

@IndradhanushGupta 好吧,接受的答案使测试比这个更pythonic。显式优于隐式。 没有其他评论者指出为什么这个答案是错误的,尽管这与 user9876 的答案错误的原因相同:失败和错误在测试代码中是不同的东西。如果您的函数在未断言的测试期间抛出异常,则测试框架会将其视为错误,而不是未断言失败。 @CoreDumpError 我理解失败和错误之间的区别,但这不会迫使你用 try/exception 构造包围每个测试吗?或者您是否建议仅对在某些条件下显式引发异常的测试执行此操作(这基本上意味着异常是预期的)。 @federicojasson 您在第二句话中很好地回答了您自己的问题。测试中的错误与失败可以分别简明地描述为“意外崩溃”与“意外行为”。您希望您的测试在您的函数崩溃时显示错误,而不是当您知道它会在给定某些输入时抛出的异常在给定不同的输入时抛出。【参考方案4】:

我是原始发帖人,我接受了 DGH 的上述答案,而没有首先在代码中使用它。

一旦我确实使用了,我意识到它需要进行一些调整才能真正完成我需要它做的事情(为了公平对待 DGH,他/她确实说过“或类似的东西”!)。

我认为值得在此处发布调整以造福他人:

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

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

我相信使用上面的代码(很大程度上基于 DGH 的回答)可以做到这一点。

【讨论】:

既然你是在澄清你的问题而不是回答它,你应该编辑它(没有回答它)。请在下面查看我的回答。 这似乎与原来的问题完全相反。 self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 应该在这种情况下完成这项工作。【参考方案5】:

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

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

这允许您测试代码中的错误情况。

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

【讨论】:

不,只有在上下文管理器块中引发异常没有时才会失败。可以通过 'with self.assertRaises(TypeError): raise TypeError' 轻松测试,通过。 @MatthewTrevor 好电话。我记得,我建议测试错误案例,而不是测试代码是否正确执行,即不会引发。我已经相应地编辑了答案。希望我能获得 +1 以摆脱亏损。 :) 注意,这也是 Python 2.7 及更高版本:docs.python.org/2/library/…【参考方案6】:

我发现猴子补丁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))

【讨论】:

什么是猴子补丁? 见en.wikipedia.org/wiki/Monkey_patch。将assertMayRaise 添加到unittest.TestSuite 后,您可以假装它是unittest 库的一部分。【参考方案7】:

您可以通过在unittest 模块中重用assertRaises 的原始实现的大约90% 来定义assertNotRaises。使用这种方法,您最终会得到一个 assertNotRaises 方法,除了其反向失败条件之外,它的行为与 assertRaises 相同。

TLDR 和现场演示

事实证明,将 assertNotRaises 方法添加到 unittest.TestCase 非常容易(编写此答案的时间是编写代码的大约 4 倍)。这是a live demo of the assertNotRaises method in action。只需like assertRaises,您可以将可调用对象和参数传递给assertNotRaises,也可以在with 语句中使用它。现场演示包含一个测试用例,证明 assertNotRaises 可以按预期工作。

详情

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

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

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 方法。

【讨论】:

IMNSHO 这应该被添加到单元测试库中..【参考方案8】:

你可以这样尝试。 尝试: self.assertRaises(无,函数,arg1,arg2) 除了: 经过 如果您不将代码放入 try 块中,它将通过异常“ AssertionError:None not raise ”并且测试用例将失败。如果将代码放入 try 块中,则测试用例将通过,这是预期的行为。

【讨论】:

【参考方案9】:
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')

【讨论】:

【参考方案10】:

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

这是一个例子:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))

【讨论】:

以上是关于Python unittest - 与assertRaises相反?的主要内容,如果未能解决你的问题,请参考以下文章

Selenium2+python自动化56-unittest之断言(assert)

Selenium2+python自动化56-unittest之断言(assert)转载

unittest----assert断言的使用

unittest断言方法的介绍

python unittest

python中的unittest有啥作用