2010-11-03 23 views
10

Ho appena iniziato a studiare su TDD e sto sviluppando un programma utilizzando una GUI di Tkinter. L'unico problema è che una volta chiamato il metodo .mainloop(), la suite di test si blocca fino a quando la finestra non viene chiusa.Come si esegue l'unittest su un'app Tkinter?

Ecco un esempio del mio codice:

# server.py 
import Tkinter as tk 

class Server(tk.Tk): 
    def __init__(self): 
     tk.Tk.__init__(self) 
     self.mainloop() 

# test.py 
import unittest 
import server 

class ServerTestCase(unittest.TestCase): 
    def testClassSetup(self): 
     server.Server() 
     # and of course I can't call any server.whatever functions here 

if __name__ == '__main__': 
    unittest.main() 

Allora, qual è il modo appropriato di applicazioni di test Tkinter? O è solo 'non farlo'?

Grazie!

risposta

1

Una cosa che si può fare è generare il mainloop in un thread separato e usare il thread principale per eseguire i test effettivi; guarda il thread mainloop come se fosse. Assicurati di controllare lo stato della finestra di Tk prima di fare il tuo asser.

Multithreading qualsiasi codice è difficile. Potresti voler spezzare il tuo programma Tk in pezzi testabili invece di testare l'intera unità contemporaneamente (il che non è in realtà un test unitario).

Vorrei infine suggerire test almeno a livello di controllo se non inferiore per il tuo programma, ti aiuterà moltissimo.

+0

Dopo averci pensato per alcuni giorni (e lavorando su altri progetti), questo ha molto più senso: avere una classe di elaborazione con cui la GUI si interfaccia dovrebbe rendere più facile testare, e forse anche più facile scrivere. –

+0

Il ciclo di eventi Tk deve essere eseguito nella thread principale. Quindi usa il thread generato per i test. – ankostis

+0

Non è TK thread-non sicuro? –

2

C'è una tecnica chiamata patch di scimmia, per cui si cambia codice in fase di esecuzione.

È possibile eseguire il patch delle scimmie sulla classe TK, in modo che mainloop non avvii effettivamente il programma.

Qualcosa di simile nella vostra test.py (non testata!):

import tk 
class FakeTk(object): 
    def mainloop(self): 
     pass 

tk.__dict__['Tk'] = FakeTk 
import server 

def test_server(): 
    s = server.Server() 
    server.mainloop() # shouldn't endless loop on you now... 

Un quadro di scherno come mock rende questo molto meno doloroso.

+1

Questo è il percorso migliore. Alla fine sono arrivato a sospettare che Tk non si spenga in modo pulito tra unittests. (Uso il modulo unittest della libreria standard.) Diventa un po 'più complicato se la classe è un complesso di widget tkinter. In TDD devi istanziare l'oggetto per il test mentre scimmia patcha qualsiasi tentativo di avviare Tk. Ciò influisce anche sull'implementazione di '__str__' e' __repr__'. – lemi57ssss

+0

Senza la logica Tk coinvolta, non è possibile essere certi che le azioni abbiano l'effetto desiderato sull'interfaccia utente. –

+0

D'accordo, ma non è un test unitario. Lo testerei in un altro modo, magari con uno script che automatizza l'interfaccia utente. –

2

@Ryan La risposta di Ginstrom cita finta. Questa ricetta Active State mostra come simulare evita il problema dell'OP della suite di test sospesa: Robust Unittesting of Tkinter Menu Items with Mocking. La suite di test in questa ricetta non include alcuna chiamata a mainloop.

+0

L'autore del post stesso ammette che "L'approccio alla fine fallì a causa di effetti collaterali non bloccabili che non potevano essere cancellati nel metodo tearDown." _ –

+0

Inoltre, sembra non eseguire il ciclo degli eventi, quindi i gestori di eventi non si attivano. Nel ["my current approarch" code] (https://github.com/trin5tensa/pigjar/blob/master/pigjar/test/test_guidialogs.py) modulo collegato lì, controlla solo lo stato iniziale. –

+0

Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il link per riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia. - [Dalla recensione] (/ recensione/post di bassa qualità/18960614) –

1

Bottom line: pompare gli eventi con il codice seguente dopo un'azione che causa un evento UI, prima di un'azione successiva che richiede l'effetto di quell'evento.


IPython fornisce una soluzione elegante senza fili è il suo gui tk implementazione del comando di magia che con ubicazione nel terminal/pt_inputhooks/tk.py.

Invece di root.mainloop(), viene eseguito root.dooneevent() è un ciclo, che verifica la condizione di uscita (un input interattivo in arrivo) ogni iterazione. In questo modo, il ciclo pari non viene eseguito quando IPython è impegnato a elaborare un comando.

Con i test, non c'è nessun evento esterno da attendere e il test è sempre "occupato", quindi è necessario eseguire manualmente (o semi-automaticamente) il ciclo in "momenti appropriati". Quali sono?

Il test mostra che senza un ciclo di eventi, è possibile modificare direttamente i widget (con <widget>.tk.call() e qualsiasi cosa lo avvolga), ma i gestori di eventi non si attivano mai. Quindi, il ciclo deve essere eseguito ogni volta che si verifica un evento e abbiamo bisogno del suo effetto - ad esempio dopo ogni operazione che cambia qualcosa.

Il codice, derivato dalla suddetta procedura IPython, sarebbe:

def pump_events(root): 
    while root.dooneevent(_tkinter.ALL_EVENTS|_tkinter.DONT_WAIT): 
     pass 

grado di trattare (eseguire dei gestori per) tutti gli eventi in sospeso, e tutti gli eventi che potrebbero derivare da quelli direttamente.

(tkinter.Tk.dooneevent() delegati al Tcl_DoOneEvent().)

Problemi correlati