代码之家  ›  专栏  ›  技术社区  ›  Karl Anderson

如何在没有错误的情况下测试扭曲的延迟错误?

  •  9
  • Karl Anderson  · 技术社区  · 14 年前

    我有一些扭曲的代码,它创建了多个延迟链。其中一些可能会在没有返回错误的情况下失败,从而使它们返回回拨链。我还没有为这段代码编写单元测试——延迟失败会导致测试在测试代码完成后失败。如何编写此代码的通过单元测试?是否期望在正常操作中可能失败的每个延迟都在将其放回回调链的链的末尾有一个errback?

    同样的事情发生在延迟列表中有一个失败的延迟时,除非我用ConsumerErrors创建延迟列表。即使使用fireonnerrback创建了deferredList,并给它一个errback,将其放回回回调链,也会出现这种情况。除了抑制测试失败和错误日志记录之外,是否还会对消费错误产生任何影响?每一个可能没有错误返回而失败的延迟都应该放一个延迟列表吗?

    示例代码的示例测试:

    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 回复  |  直到 14 年前
        1
  •  15
  •   Jean-Paul Calderone    14 年前

    关于这个问题,审判有两件重要的事情。

    首先,如果一个测试方法在运行时记录了一个失败,那么它将不会通过。由于故障结果而被垃圾收集的延迟会导致记录故障。

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

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

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

    这一点很重要,因为第一种情况,即延迟被垃圾收集并产生故障结果,意味着发生了一个没有人处理的错误。这有点类似于当异常到达程序的顶层时,Python报告堆栈跟踪的方式。

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

    不过,有一些工具可以处理每一种情况。毕竟,如果你不能通过一个API的测试,而这个API返回了一个延迟的、有时会因失败而触发的测试,那么你就永远无法测试你的错误代码。那将是一个非常可悲的情况。:)

    因此,处理这一问题的两个工具中更有用的是 TestCase.assertFailure . 对于希望返回将在失败时激发的延迟的测试,这是一个助手:

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

    这个测试将通过,因为 d 是否在包装ValueError时失败而激发。如果 D 如果已激发,但结果成功,或者包装其他异常类型失败,则测试仍将失败。

    接下来,有 TestCase.flushLoggedErrors . 当你测试一个API的时候, 想象上的 记录错误。毕竟,有时您确实想通知管理员有问题。

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

    这允许您检查记录的故障,以确保日志代码正常工作。它还告诉Trial不要担心你冲洗过的东西,这样它们就不会再导致测试失败。(The gc.collect() 调用存在,因为在对延迟进行垃圾收集之前,不会记录错误。在cpython上,由于引用计数GC行为,它将立即被垃圾收集。但是,在jython、pypy或任何其他没有引用计数的python运行时上,您不能依赖它。)

    此外,由于垃圾收集几乎可以在任何时候发生,您有时可能会发现您的某个测试失败,因为错误是由 早期的 测试是在以后的测试执行期间被垃圾收集的。这通常意味着您的错误处理代码在某种程度上是不完整的——您缺少了一个errback,或者您未能在某个地方将两个延迟链接在一起,或者您让测试方法在它开始的任务实际完成之前完成——但是报告错误的方式有时会使跟踪有问题的代码变得困难。试用 --force-gc 选项可以帮助解决此问题。它使trial在每个测试方法之间调用垃圾收集器。这将显著降低您的测试速度,但它会导致错误被记录到实际触发它的测试中,而不是随后的任意测试中。