Python unittest - 与assertRaises相反?

gla*_*con 341 python unit-testing

我想编写一个测试来确定在给定的环境中不会引发异常.

这是简单的测试,如果一个异常引发...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 
Run Code Online (Sandbox Code Playgroud)

......但你怎么能做相反的事情.

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

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 
Run Code Online (Sandbox Code Playgroud)

DGH*_*DGH 358

def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")
Run Code Online (Sandbox Code Playgroud)

  • @hiwaylon - 不,这是事实上的正确解决方案.user9876提出的解决方案在概念上存在缺陷:如果你测试不会引发'ValueError`,但是会引发`ValueError`,那么你的测试必须以失败条件退出,而不是错误条件.另一方面,如果在运行相同的代码时你会引发一个`KeyError`,这将是一个错误,而不是一个失败.在python中 - 与其他语言不同 - 异常通常用于控制流,这就是我们确实使用`except <ExceptionName>`语法的原因.在这方面,user9876的解决方案完全错误. (31认同)
  • @ Shay,IMO,您应该始终将测试文件本身从覆盖率报告中排除(由于按定义它们几乎总是运行100%,因此您会人为地夸大报告) (3认同)
  • 要求 100% 覆盖测试文件捕获对我来说已经发现了很多错误,否则这些错误可能不会被发现。 (2认同)

S.L*_*ott 62

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

这是默认假设 - 不会引发异常.

如果你别说什么,那就是在每一次测试中都假设的.

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

  • 没有其他评论者指出*为什么*这个答案是错误的,尽管这与user9876的答案错误的原因相同:测试代码中的失败和错误是不同的.如果你的函数*是*在没有断言的测试期间抛出异常,那么测试框架会将其视为**错误**,而不是不断言. (15认同)
  • @IndradhanushGupta嗯,接受的答案使得测试比这个更加pythonic.显式优于隐式. (7认同)
  • @federicojasson你在第二句中很好地回答了你自己的问题.测试中的错误与失败分别简洁地描述为"意外崩溃"与"非预期行为".您希望测试在函数崩溃时显示错误,但是当给定不同的输入时,如果您知道它的异常将抛出某些输入,则不会抛出该错误. (4认同)
  • @CoreDumpError 我理解失败和错误之间的区别,但这不会迫使你用 try/exception 构造来包围 **每个测试**吗?或者您是否建议仅针对在某些条件下显式引发异常的测试(这基本上意味着异常是*预期的*)才这样做。 (2认同)

use*_*876 52

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

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)
Run Code Online (Sandbox Code Playgroud)

  • 故障和错误在概念上是不同的.此外,因为在python异常通常用于控制流程,如果你破坏了逻辑或代码,这将很难一目了然(=没有探索测试代码)... (34认同)
  • @ user9876 - 没有.测试出口条件是3(通过/ nopass /错误),而不是2,因为你似乎错误地相信.错误和失败之间的差异是巨大的,将它们视为相同只是糟糕的编程.如果你不相信我,只需看看测试跑步者的工作方式以及他们为失败和错误实施的决策树.python的一个很好的起点是pytest中的[xfail`装饰器](http://pytest.org/latest/skipping.html). (17认同)
  • 您的测试要么通过,要么不通过。如果没有通过,你就必须去修复它。无论它被报告为“失败”还是“错误”,基本上都是无关紧要的。有一个区别:通过我的回答,您将看到堆栈跟踪,以便您可以看到 PathIsNotAValidOne 被抛出的位置;使用接受的答案,您将不会获得该信息,因此调试会更加困难。(假设 Py2;不确定 Py3 是否在这方面更好)。 (4认同)
  • 我想这取决于您如何使用单元测试。我的团队使用单元测试的方式必须全部通过。(使用连续的集成机器运行所有单元测试的敏捷编程)。我知道测试用例可以报告“通过”,“失败”或“错误”。但是从总体上来说,对我的团队而言真正重要的是“所有单元测试都通过了吗?” (即“詹金斯(Jenkins)是绿色的吗?”)。因此,对于我的团队而言,“失败”和“错误”之间没有实际区别。如果以不同的方式使用单元测试,则可能会有不同的要求。 (3认同)

gla*_*con 14

我是原始海报,我在没有首先在代码中使用它的情况下接受了DGH的上述答案.

一旦我使用了,我意识到需要做一些调整才能真正做我需要做的事情(对DGH来说,他/她确实说"或类似的东西"!).

为了别人的利益,我认为值得在这里发布调整:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)
Run Code Online (Sandbox Code Playgroud)

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

我相信使用上面的代码(严重依赖DGH的答案)会这样做.

  • 这似乎与原始问题完全相反.`self.assertRaises(PathIsNotAValidOne,MyObject,sInvalidPath)`应该在这种情况下完成工作. (12认同)
  • 既然你正在澄清你的问题而没有回答它你应该编辑它(没有回答).请看下面的答案. (2认同)

tel*_*tel 8

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

TLDR和现场演示

事实证明,向其中添加assertNotRaises方法非常容易unittest.TestCase(编写此答案所花的时间大约是代码的4倍)。这是该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
Run Code Online (Sandbox Code Playgroud)

通常,您可以通过从中继承来定义测试用例类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
Run Code Online (Sandbox Code Playgroud)

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

  • IMNSHO 这应该添加到单元测试库中。 (6认同)