wxpython und untittest: event mit Exception, die assertRaises nicht bemerkt

Plattformunabhängige GUIs mit wxWidgets.
Antworten
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Hallo,

ich benötige Hilfe bei folgendem Problem: Ich habe einen wxpython-Dialog, der als Ergebnis einer Validierung eine TypeError-Exception ausgibt, wenn der User den OK-Button klickt. Ich teste diesen Dialog mit unittest, allerdings arbeitet der Test nicht so, wie ich es erwarte. Die Ausgabe des Tests zeigt, dass die Exception auftritt und trotzdem sagt unittest, das der Test FAILed, weil "TypeError not raised". Hier ist die Ausgabe:

[codebox=text file=Unbenannt.txt]"C:\Program Files (x86)\Python\python.exe" test.py
Traceback (most recent call last):
File "test.py", line 22, in on_ok
raise TypeError( 'TypeError raised' )
TypeError: TypeError raised
F
======================================================================
FAIL: test_should_raise (__main__.CDlgTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 34, in test_should_raise
self._dut.m_button_ok.GetEventHandler().ProcessEvent( event )
AssertionError: TypeError not raised

----------------------------------------------------------------------
Ran 1 test in 0.005s

FAILED (failures=1)[/code]
Hier mein (stark reduziertes) Programm:

Code: Alles auswählen

import unittest
import wx

class CDlgBase ( wx.Dialog ):
    """The UI"""
    def __init__( self, parent ):
        wx.Dialog.__init__ ( self, parent )
        bSizerTest = wx.BoxSizer( wx.VERTICAL )
        self.m_button_ok = wx.Button( self, wx.ID_ANY )
        bSizerTest.Add( self.m_button_ok, 0 )
        self.SetSizer( bSizerTest )
        # Connect Events
        self.m_button_ok.Bind( wx.EVT_BUTTON, self.on_ok )
    def on_ok( self, event ):
        event.Skip()

class CDlg( CDlgBase ) :
    """The dialog"""
    def __init__(self, parent):
        super( CDlg, self).__init__(parent)
    def on_ok(self, event):
        # The exception should be verified in the test `test_should_raise()`.
        raise TypeError( 'TypeError raised' )

class CDlgTest( unittest.TestCase ) :
    """The test class"""
    def setUp(self):
        self._dut = CDlg(None)
    def test_should_raise(self):
        """The test to verify raising the TypeError exception in the event 
        method `on_ok()`. this is the test method wich works not as expected."""
        event = wx.CommandEvent( wx.EVT_BUTTON.evtType[0] )
        event.SetEventObject( self._dut.m_button_ok )
        with self.assertRaises( TypeError ) :
            """Simulate an "OK" click. `on_ok()` will be executed 
            and raises the TypeError exception."""
            self._dut.m_button_ok.GetEventHandler().ProcessEvent( event )

if __name__ == '__main__':
    app = wx.App()
    tests = [ unittest.TestLoader().loadTestsFromTestCase( CDlgTest) ]
    unittest.TextTestRunner().run(unittest.TestSuite(tests) )
Was mache ich falsch?
Danek
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Humbalan: das ist die generelle Schwierigkeit beim Testen von GUIs. Exceptions, die in Event-Handlern ausgelöst werden, sollen ja normalerweise nicht das ganze Programm mit sich reißen. Daher werden sie vom Scheduler abgefangen und landen nie in Deiner Test-Routine. Am besten sind die Event-Handler nur dünne Schichten über der Logik, die man separat ohne GUI testet.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Das hört sich so an, als hätte ich mit der Exception einen Fehler gemacht. Ist es also besser, den Validator mit einem Rückgabewert zu versehen, statt im Fehlerfall eine Exception zu erzeugen?
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

In einem anderen Forum habe ich auf diese Frage folgende Antwort von Robin Dunn bekommen (von mir übersetzt):
See: https://wiki.wxpython.org/CppAndPythonSandwich

Exceptions werden nicht durch den Call-Stack der C++-Layer nach oben gereicht. Wenn Python die Steuerung an C++ zurück gibt, dann wird überprüft, ob es eine unbearbeitete Python-Exception gab. Wenn das der Fall ist, dann wird sie ausgegeben und dann gelöscht.

Eine Möglichkeit, damit in Modultests umzugehen: die Exception im eigenen Event-Handler abfangen und ein Flag setzen. Zurück im Test-Code muss dann getestet werden, ob dieses Flag gesetzt ist.
Diese Antwort war sehr hilfreich, die Problematik vollständig zu verstehen. Der Artikel beim angegeben Link ist auch lesenswert.
BlackJack

@Humbalan: Wobei das eher ein Hack ist und man eigentlich GUIs auch nicht auf diese Weise mit Unittests testet. Du würdest dabei für *Tests* zusätzlichen Code in die normale Programmlogik einfügen, die ausserhalb der Tests unbenutzt ist.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

@Blackjack: Danke für Deinen Beitrag. Ja, das stimmt, Robins Vorschlag ist hacky. Ich bin immer mehr davon überzeugt, dass es wohl besser ist, meine Validatoren zu re-designen und (wie es in allen Beispielen auch gezeigt wird) mit einem Returncode zu versehen. Ist nicht ganz trivial, denn bei den Exceptions hatte ich noch zusätzlich Daten mitgegeben. Aber der Vorteil wäre, dass es dann keine eigenen Exceptions mehr in Event-Handlern gibt. Habe mich noch nicht endgültig entschieden.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Humbalan: Event-Handler haben auch keine Rückgabewerte. Was meinst Du mit Validatoren? Was willst Du mit Deinen Tests eigentlich erreichen?
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

@Sirius3:
Was willst Du mit Deinen Tests eigentlich erreichen?
Ganz grob gesagt: ich möchte sicherstellen, dass der Dialog, den ich teste, das spezifizierte Verhalten zeigt. Ich möchte mir ersparen, bei jeder SW-Änderungen die betroffenen Dialog "händisch" zu testen, dazu habe ich nicht die Zeit und das ist mir auch zu langeweilig. In dem hier beschriebenen Test geht es v. a. darum, dass unter bestimmten Voraussetzungen eine im Test simulierte Benutzereingabe zu einer Exception führt, und unter bestimmten anderen Voraussetzungen diese Exception nicht auftritt. Die Exception tritt auf, wenn der "Benutzer" hat eine Eingabe gemacht hat, die zurückgewiesen werden muss.
Was meinst Du mit Validatoren?
Damit ich die Frage richtig verstehe, lass mich bitte eine Gegenfrage stellen: weißt Du nichts über Validatoren oder verstehst Du nur nicht, was ich damit mache?
BlackJack

@Humbalan: Also ich verstehe nicht was Du damit machst, denn im Beispiel ist ja keiner. Da ist nirgends ein `wx.Validator` oder ein Aufruf von `wx.Window.Validate()` zu sehen und auch keine Standard-OK-Schaltfläche die `OnOk()` aufrufen würde.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

@BlackJack: Einen Validator habe ich nirgends im Code gezeigt, um den Code-Schnipsel möglichst kurz zu halten. Im tatsächlichen Programm ist jedes Dialogfeld mit einem Validator verknüpft.

Ich meinte, besonders schlau zu sein, abweichend von den Beispielen im Web den Validator nicht mit einem Returnwert True oder False zu versehen, sondern bei entdeckten Fehlern eine Exception zu erzeugen :oops: . Für das angegebene Beispiel habe ich in der on_ok()-Methode abweichend von meinem tatsächlichen Code eine TypeError-Exception "hart codiert" eingefügt, weil es nicht auf die Funktionsweise des Validators ankam, sondern auf die Exception in der Event-Methode.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Humbalan: solangsam wird mir das alles klarer. Du kannst von dem, was ein Validator zurückliefert nicht abweichen, weil Validatoren werden tief in der Prozessierungslogik von Dialogfeldern verwendet, und die erwarten eben als Rückgabewert True oder False. Auf der anderen Seite rufst Du ja auch die Validatoren gar nicht auf, hast also auch gar keinen Code, der so eine Exception verarbeiten könnte.
Zudem haben Validatoren mit Event-Handlern rein gar nichts zu tun. Zeig doch mal Deinen wirklichen Code und wie Du da versuchst, Validatoren zu benutzen.
BlackJack

@Humbalan: Ich denke um einen Validator in einem Unittest zu testen müsste man wahrscheinlich auf Mock-Objekte zurückgreifen an denen man dann überprüfen kann ob der so mitspielt wie man das erwartet. Eben weil das alles so tief im Rahmenwerk steckt.

Aber wie schon mal angedeutet willst Du hier vielleicht auch nicht wirklich Unittests. Das sind vielleicht eher Akzeptanztests und eventuell ist das ein Testwerkzeug das die Verwendung der GUI und deren Reaktionen prüft, passender. Dann interessiert nicht (direkt) wann die Validatoren True oder False liefern, sondern was bei welcher Eingabe passiert wenn man Ok klickt, beziehungsweise ja auch schon was bei welchen Daten passiert wenn der Dialog angezeigt wird. Werden die Daten richtig angezeigt, kommt das Fenster mit der Fehlermeldung, und so weiter.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Danke für Eure Hilfe.

@BlackJack: Es stimmt, ich möchte bei meinen Dialog-Tests nachweisen, dass die Dialoge wie erwartet auf richtige oder falsche Eingaben reagieren und bei "OK"-Klick die erfolgten Eingaben zur Verfügung stehen. Dabei sind mir tatsächlich Returnwerte von Validatoren egal. Und tatsächlich habe ich dirch die bisherige Löstung bemerkt, dass ich nicht wirklich verstanden habe, wie Validatoren funktionieren. Und du hast Recht, ich sollte nicht sagen, ich möchte etwas testen, und das mache ich mit unittetst :D, eher ich möchte etwas testen, was ist am geeignetsten dafür. Hast Du einen Tip?

@Sirius3: Meinen Code zu zeigen wird schwierig. Nimm mir's nicht übel, aber es wäre mir jetzt zu viel Arbeit, den Code so aufzubereiten, dass er hier nicht zu viel Platz verbraucht aber nicht so knapp wird, dass er nicht mehr durchschaubar ist. Aber vielleicht geht es, wenn ich kurz beschreibe, wie ich vorgehe. Immerhin habe ich durch Deine Antworten und die von BackJack einige Ideen, was ich falsch gemacht habe.

1. Schon beschrieben: meine Validate()-Methoden der Validator-Klassen haben keinen Returnwert. Ich glaubte, Fehler mit Exceptions feststellen zu können.
2. Meine Event-Methode des OK-Buttons heißt nicht OnOk(), sondern on_ok(). Und jetzt wundert es mich nicht mehr, dass ich die Validate()-Methode in den Event-Methoden explizit aufrufen musste mit z. B. control.GetValidator().Validate(). Ich glaubte, die Verknüpfung meiner eigenen Event-Methoden mit den jeweiligen Dialogfeldern durch Bind() sei ausreichend. Das war wohl ein Trugschluss.

Der Grund für 1.: Die richtige Art, Validate() zu programmieren, liefert mir zwar die Information, dass die Eingabe in einem Dialogfeld falsch ist. Ich benötige aber gelegentlich eine Information über den Charakter des Fehlers (Limit überschritten, Syntax falsch, ungültige Intervalle), um differenzierte Fehlermeldungen auszugeben. Beispiel ist ein Datumfeld, das leer ist, wenn das Datum ungültig ist, ein Min- und ein Max-Datum definiert ist und ein oder mehere Zeiträume im Min-Max-Breich auch noch verboten sind (vereinbarte Anforderung). In der Validate()-Methode wurden diese Bedingungen analysiert und bei fehlerhafter Eingabe ein False geliefert. Jetzt soll der User erfahren, was er falsch gemacht hat (vereinbarte Anforderung):
  • Du hast nichts eingegeben, eine Eingabe ist aber erforderlich
  • die Eingabe war kein gültiges Datum
  • Das eingegebene Datum ist vor/nach dem ... und ist daher nicht erlaubt
  • Das eingegebene Datum ist zwischen ... und ... und ist daher nicht erlaubt
Dazu ist die Information "falsch" nicht ausreichend. Jetzt müsste ich noch einmal dieselbe Analyse machen, die der Validator schon gemacht hat? Wäre doof. Also benötige ich zu dem False aus Validate() weitere Informationen. Die konnte ich der Exception einfach mitgeben. Auch hier würde ich mich über Tips für eine geeignetere Methode freuen.
BlackJack

Ich bin kein Experte für `wxWidgets`, aber wenn ich das richtig sehe geht das Rahmenwerk davon aus, dass der Validator auch dafür verantwortlich ist den Benutzer zu informieren. Bei der Beschreibung der `wx.Validator.Validate()`-Methode steht der Satz „Return false if the value in the window is not valid; you may pop up an error dialog.“. Validatoren für Eingabeelemente könnten die dann zum Beispiel auch mit einem roten Hintergrund oder Rahmen versehen und/oder einen Tooltip mit der Problembeschreibung füttern.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

BlackJack hat geschrieben:Bei der Beschreibung der `wx.Validator.Validate()`-Methode steht der Satz „Return false if the value in the window is not valid; you may pop up an error dialog.“.
Kenn ich, aber hatte bisher gedacht, ich will nicht in einer allgemeingültigen Validator-Klasse eine produktspezifische Ausgabe machen. Bisher (mit der Exception-Lösung) war der Validator wiederverwendbar für andere Produkte, die zwar vergleichbare Bedinungen für Fehler haben, aber andere Fehlerreaktionen erforderlich machen. Oder bei meinem Datum-Beispiel: Ein Feld reagiert auf die Verletzung aller vier Bedingungen, ein anders nur auf die Min-Max-Verletzung.

Ich habe nun angefangen, meinen Code so umzubauen:

1. die Validate()-Methoden bekommen die erforderliche Form, die Exceptions fliegen raus.
2. alle Fehlerreaktionen werden in validate_callback()-Methoden verpackt, die dann - falls erforderlich - Feld-spezifisch sind.
3. die Validate()-Methoden geben dem Callback Informationen über die Fehlerqualität mit, um die Anforderungen zu erfüllen zu können, die ich im Datum-Beispiel formuliert hatte. Dazu musste ich der Callback-Methode zusätzliche Parameter verpassen.

Leider habe ich nirgendwo eine Info gefunden, ob auch die Validate()-Methode eine bestimmte Form haben muss (wie in https://wiki.wxpython.org/Validator%20f ... Attributes beschrieben ist. Dort wurde sie so vorgeschlagen:

Code: Alles auswählen

def validationCB( obj, attrName, value, flRequired, flValid )
Ich habe die Signatur noch durch einen Paramter `err` erweitert, in dem der aufgetretene Fehler näher bezeichnet werden kann (wie auch immer).
BlackJack

@Humbalan: Die Callbacks dort im Wiki haben ja nichts mit wxWidgets zu tun. Die sind ja eine Erfindung vom Autor der Wikiseite. Also wird man ansonsten keine weiteren Anforderungen an die Form dieser Funktionen/Methoden finden.
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Danke, damit habe ich wohl alles, was ich brauche. :D
Antworten