如何通过试验测试没有错误的Twisted Deferred错误?

时间:2010-07-14 20:24:40

标签: python unit-testing testing twisted

我有一些Twisted代码可以创建多个Deferred链。其中一些可能会失败而没有errback,这会将它们放回回调链。我无法为此代码编写单元测试 - 失败的Deferred导致测试在测试代码完成后失败。如何为此代码编写通过单元测试?是否期望在正常操作中可能失败的每个Deferred都应该在链的末尾有一个errback,这会将它放回到回调链上?

当DeferredList中的Deferred失败时,会发生同样的事情,除非我使用consumeErrors创建DeferredList。即使使用fireOnOneErrback创建DeferredList并给出一个将其放回回调链的errback,也是如此。除了抑制测试失败和错误记录之外,是否对consumeErrors有任何影响?如果每个Deferred在没有errback的情况下可能会失败,那么是否应该放置DeferredList?

示例代码的示例测试:

from twisted.trial import unittest
from twisted.internet import defer

def get_dl(**kwargs):
    "Return a DeferredList with a failure and any kwargs given."
    return defer.DeferredList(
        [defer.succeed(True), defer.fail(ValueError()), defer.succeed(True)],
        **kwargs)

def two_deferreds():
    "Create a failing Deferred, and create and return a succeeding Deferred."
    d = defer.fail(ValueError())
    return defer.succeed(True)


class DeferredChainTest(unittest.TestCase):

    def check_success(self, result):
        "If we're called, we're on the callback chain."        
        self.fail()

    def check_error(self, failure):
        """
        If we're called, we're on the errback chain.
        Return to put us back on the callback chain.
        """
        return True

    def check_error_fail(self, failure):
        """
        If we're called, we're on the errback chain.
        """
        self.fail()        

    # This fails after all callbacks and errbacks have been run, with the
    # ValueError from the failed defer, even though we're
    # not on the errback chain.
    def test_plain(self):
        """
        Test that a DeferredList without arguments is on the callback chain.
        """
        # check_error_fail asserts that we are on the callback chain.
        return get_dl().addErrback(self.check_error_fail)

    # This fails after all callbacks and errbacks have been run, with the
    # ValueError from the failed defer, even though we're
    # not on the errback chain.
    def test_fire(self):
        """
        Test that a DeferredList with fireOnOneErrback errbacks on failure,
        and that an errback puts it back on the callback chain.
        """
        # check_success asserts that we don't callback.
        # check_error_fail asserts that we are on the callback chain.
        return get_dl(fireOnOneErrback=True).addCallbacks(
            self.check_success, self.check_error).addErrback(
            self.check_error_fail)

    # This succeeds.
    def test_consume(self):
        """
        Test that a DeferredList with consumeErrors errbacks on failure,
        and that an errback puts it back on the callback chain.
        """
        # check_error_fail asserts that we are on the callback chain.
        return get_dl(consumeErrors=True).addErrback(self.check_error_fail)

    # This succeeds.
    def test_fire_consume(self):
        """
        Test that a DeferredList with fireOnOneCallback and consumeErrors
        errbacks on failure, and that an errback puts it back on the
        callback chain.
        """
        # check_success asserts that we don't callback.
        # check_error_fail asserts that we are on the callback chain.
        return get_dl(fireOnOneErrback=True, consumeErrors=True).addCallbacks(
            self.check_success, self.check_error).addErrback(
            self.check_error_fail)

    # This fails after all callbacks and errbacks have been run, with the
    # ValueError from the failed defer, even though we're
    # not on the errback chain.
    def test_two_deferreds(self):
        # check_error_fail asserts that we are on the callback chain.        
        return two_deferreds().addErrback(self.check_error_fail)

1 个答案:

答案 0 :(得分:15)

关于此问题的审判有两个重要的事项。

首先,如果在运行时记录失败,则测试方法不会通过。使用Failure结果进行垃圾收集的延迟会导致失败。

其次,如果Deferred触发失败,则返回Deferred的测试方法将不会通过。

这意味着这些测试都不能通过:

def test_logit(self):
    defer.fail(Exception("oh no"))

def test_returnit(self):
    return defer.fail(Exception("oh no"))

这一点非常重要,因为第一种情况,即Deferred被收集的故障是一个失败的结果,意味着发生了无人处理的错误。这有点类似于Python在报告程序的最高级别时报告堆栈跟踪的方式。

同样,第二个案例是审判提供的安全网。如果同步测试方法引发异常,则测试不会通过。因此,如果试验测试方法返回Deferred,则Deferred必须具有成功结果才能通过测试。

但是,有一些工具可以处理这些案例。毕竟,如果你无法对返回延迟触发失败的API进行传递测试,那么你永远无法测试你的错误代码。那将是一个非常悲伤的情况。 :)

因此,处理此问题的两个工具中更有用的是TestCase.assertFailure。这是一个帮助测试,希望返回一个将失败的Deferred:

def test_returnit(self):
    d = defer.fail(ValueError("6 is a bad value"))
    return self.assertFailure(d, ValueError)

此测试将通过,因为d会触发包含ValueError的Failure。如果d使用成功结果或使用Failure包装其他异常类型而触发,则测试仍会失败。

接下来,有TestCase.flushLoggedErrors。这是为了测试一个假设的API来记录错误。毕竟,有时您确实想告知管理员存在问题。

def test_logit(self):
    defer.fail(ValueError("6 is a bad value"))
    gc.collect()
    self.assertEquals(self.flushLoggedErrors(ValueError), 1)

这使您可以检查记录的故障,以确保您的日志记录代码正常工作。它还告诉试用版不要担心你刷新的东西,因此它们将不再导致测试失败。 (gc.collect()调用是存在的,因为在Deferred被垃圾收集之前不记录错误。在CPython上,由于引用计数GC行为,它将立即被垃圾收集。但是,在Jython或PyPy上或任何其他没有引用计数的Python运行时,你都不能依赖它。)

此外,由于垃圾收集几乎可以在任何时间发生,您有时可能会发现其中一个测试失败,因为之前的测试创建的延迟记录了错误执行后来的测试。这几乎总意味着你的错误处理代码在某种程度上是不完整的 - 你错过了一个错误,或者你无法将两个Deferred链接在一起,或者你让测试方法在它开始的任务实际完成之前完成 - 但是报告错误的方式有时会导致难以追踪违规代码。试用--force-gc选项可以帮助解决这个问题。它导致试验在每个测试方法之间调用垃圾收集器。这会显着减慢你的测试速度,但它应该导致错误被记录在实际触发它的测试中,而不是随后的任意测试。