2010-07-14 6 views
9

Ich habe einige Twisted-Code, der mehrere Ketten von Deferreds erstellt. Einige von diesen können fehlschlagen, ohne einen Fehler zu haben, der sie zurück in die Rückrufkette bringt. Ich war nicht in der Lage, einen Komponententest für diesen Code zu schreiben - der fehlgeschlagene Verzögerter bewirkt, dass der Test nach Abschluss des Testcodes fehlschlägt. Wie kann ich einen bestandenen Unit-Test für diesen Code schreiben? Wird erwartet, dass jeder Deferred, der im normalen Betrieb ausfallen könnte, am Ende der Kette einen Fehlerrücklauf haben sollte, der ihn zurück in die Callback-Kette bringt?Wie können verdrillte verzögerte Fehler ohne Fehler mit der Testversion getestet werden?

Das Gleiche passiert, wenn eine DeferredList in einer DeferredList fehlschlägt, es sei denn, ich erstelle die DeferredList mit consumeErrors. Dies ist auch dann der Fall, wenn die DeferredList mit fireOnEneErrback erstellt wird und ein Fehler zurückgegeben wird, der sie zurück in die Callback-Kette stellt. Gibt es irgendwelche Implikationen für consumeErrors, abgesehen von der Unterdrückung von Testfehlern und der Fehlerprotokollierung? Sollte jeder Deferred, der ohne einen Fehler fehlschlagen könnte, eine DeferredList setzen?

Beispiel Tests von Beispielcode:

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) 

Antwort

15

Es gibt zwei wichtige Dinge über Versuch auf diese Frage bezogen.

Zuerst wird eine Testmethode nicht bestehen, wenn ein Fehler protokolliert wird, während er ausgeführt wird. Deferred-Werte, die mit einem Failure-Ergebnis gesammelt wurden, führen dazu, dass der Fehler protokolliert wird.

Zweitens wird eine Testmethode, die eine Deferred zurückgibt, nicht bestanden, wenn die Deferred mit einem Fehler ausgelöst wird. Diese

bedeutet, dass keine dieser Prüfung gehen:

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

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

Dies ist wichtig, weil der erste Fall, der Fall eines latenten ist Müll mit einem Ausfall Ergebnis gesammelt, bedeutet, dass ein Fehler passiert ist, dass niemand abgewickelt. Es ähnelt der Art und Weise, wie Python einen Stack-Trace meldet, wenn eine Ausnahme die oberste Ebene Ihres Programms erreicht.

Ebenso ist der zweite Fall ein Sicherheitsnetz, das durch Versuch zur Verfügung gestellt wird. Wenn eine synchrone Testmethode eine Ausnahme auslöst, wird der Test nicht bestanden. Wenn also eine Versuchsmethode ein Zurückgestelltes zurückgibt, muss das Zurückgestellte ein Erfolgsergebnis haben, damit der Test bestanden wird.

Es gibt jedoch Tools für den Umgang mit jedem dieser Fälle. Wenn Sie keinen Passtest für eine API durchführen können, die einen Deferred-Befehl zurückgegeben hat, der manchmal mit einem Fehler ausgelöst wurde, könnten Sie Ihren Fehlercode niemals testen. Das wäre eine ziemlich traurige Situation. :)

Also, die nützlichere der beiden Tools für den Umgang mit diesem ist TestCase.assertFailure. Dies ist ein Helfer für die Tests, die zurückkehren wollen eine latente, dass bei einem Ausfall Feuer geht:

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

Dieser Test wird passieren, weil d tut Feuer mit einem Ausfall eines Valueerror gewickelt wird. Wenn d mit einem Erfolgsergebnis oder mit einem Fehler beim Umhüllen eines anderen Ausnahmetyps ausgelöst wurde, würde der Test weiterhin fehlschlagen.

Als nächstes gibt es TestCase.flushLoggedErrors. Dies ist, wenn Sie eine API testen, die angenommen ist, um einen Fehler zu protokollieren. Schließlich möchten Sie manchmal einem Administrator mitteilen, dass ein Problem vorliegt.

Damit können Sie die protokollierten Fehler überprüfen, um sicherzustellen, dass Ihr Protokollierungscode ordnungsgemäß funktioniert. Es erklärt auch, sich nicht um die Dinge zu kümmern, die Sie geleert haben, so dass sie den Test nicht mehr scheitern lassen. (Der Aufruf gc.collect() ist da, weil der Fehler nicht protokolliert wird, bis der Deferred Garbage Collected ist. Bei CPython wird sofort Garbage Collected durch das GC-Verhalten der Referenzzählung erfasst. Allerdings auf Jython oder PyPy oder einer anderen Python-Laufzeit ohne Referenzzählung. Sie können sich nicht darauf verlassen.)

Da die Garbage-Collection zu jeder Zeit auftreten kann, kann es vorkommen, dass einer Ihrer Tests fehlschlägt, weil ein Fehler von Deferred created by protokolliert wird Ein früherer Test wird während der Ausführung des späteren Tests als Müll gesammelt. Dies bedeutet immer, dass Ihr Fehlerbehandlungscode in gewisser Weise unvollständig ist - Sie haben einen Fehler verloren, oder Sie haben es versäumt, zwei Verzögerter miteinander zu verketten, oder Sie lassen Ihre Testmethode beenden, bevor die Aufgabe, die sie gestartet hat, tatsächlich beendet wird Die Art, wie der Fehler gemeldet wird, macht es manchmal schwierig, den fehlerhaften Code aufzuspüren. Die Option --force-gc der Studie kann dabei helfen. Es verursacht Versuch, den Garbage Collector zwischen jeder Testmethode aufzurufen. Dies wird Ihre Tests erheblich verlangsamen, aber es sollte dazu führen, dass der Fehler für den Test protokolliert wird, der ihn auslöst, und nicht für einen beliebigen späteren Test.

+0

Große Antwort, aber Sie könnten auch "--force-gc" erwähnen. – Glyph

+0

Guter Anruf, hinzugefügt. –

+0

Dies passiert auch beim Aufruf von log.err mit einer Fehlerinstanz, richtig? – Chris

Verwandte Themen