Welcome, guest | Sign In | My Account | Store | Cart

This recipe shows a skeleton unittest fixture that calls a widget's command using invoke without starting tkinter's mainloop. (Tcl8.6.3/Tk8.6.3.)

Python, 55 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# -*- coding: utf-8 -*-
# clickinvoke.py


import sys
import tkinter as tk
import tkinter.ttk as ttk


class ClickInvoke(tk.Tk):
    def __init__(self):
        super().__init__()
        self.b1 = ttk.Button(text='Button 1', name='b1', command=self.click1)
        self.b1.pack(side='left')
        self.b2 = ttk.Button(text='Button 2', name='b2', command=self.click2)
        self.b2.pack(side='left')

    def click1(self):
        print('Button 1 clicked.')

    def click2(self):
        print('Button 2 clicked.')
        self.b1.invoke()


def main():
    app = ClickInvoke()
    app.mainloop()


if __name__ == '__main__':
    sys.exit(main())

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# -*- coding: utf-8 -*-
# test_clickinvoke.py


import unittest

import clickinvoke


class TestClickInvoke(unittest.TestCase):
    def setUp(self):
        self.app = clickinvoke.ClickInvoke()

    def tearDown(self):
        self.app.destroy()

    def test_button1(self):
        self.app.children['b1'].invoke()

    def test_button2(self):
        self.app.children['b2'].invoke()

Please Note: This recipe was written while I was trying to grapple with the problems of applying TDD to tkinter. I no longer consider it good practice.

  • The approach ultimately failed because of unmockable side-effects that could not be cleared in the tearDown method. (tkinter predates TDD and was not originally designed with TDD in mind)
  • Test calls into tkinter were taking around 150 to 200 ms for each test unit.
  • Although the tests were useful in testing that tkinter's behavior match my understanding of tkinter they looked more like integration tests than unit tests. For my current approach see test_guidialogs.py and guidialogs.py.

dialog.py is based on Fredrik Lundh's dialogs.

Original Discussion

This code has been tested under Python 3.4 and MS Windows 7.

The main module clickinvoke.py produces a window with two buttons. Clicking on button 1 prints the text 'Button 1 clicked.'. Clicking on button 2 prints the text 'Button 2 clicked.' Button 2 also prints 'Button 1 clicked.' because the click2 method has the line self.b1.invoke()

The unittest fixture TestClickInvoke creates a ClickInvoke instance but does not call its mainloop method. The purpose of the two unittests is simply to make sure that when a widget is clicked by the user that the correct command is called. The unittests 'click' the buttons using their invoke methods. If this were production code the print statements would be removed and the methods called by the button's commands would be mocked.

If this were to be used as a basis for a TDD approach to tkinter development it has two problems. The name of the widget is predetermined by the test and so the widget must be explicitly named. I don't see this as too much of a problem.

A worse problem is predetermining the structure of the window. In the example code the test expects buttons to be placed directly in Tk's main window (see self.app.children['b2']). If the implementer were to add an intermediate frame the test would break. In other words the implementer might want to use a structure like Tk_main.Frame.Buttonbox.Button1 instead of the simple Tk_main.Button1 illustrated in the sample code.

This could be avoided by searching the widget tree for the widget name every time. This would exchange speed for a reduction in test fragility. For this to work the widget must be assigned a unique name within the scope of the root widget used for the tree search.