Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 247bd5e

Browse files
committed
Issue18130: Test class idlelib.configSectionNameDialog.GetCfgSectionNameDialog.
Fix bug in existing human test and add instructions; fix two bugs in tested code; remove redundancies, add spaces, and change two internal method names. Add mock_tk with mocks for tkinter.Variable subclasses and tkinter.messagebox. Use mocks in test_config_name to unittest methods that are otherwise gui-free.
1 parent a534fc4 commit 247bd5e

3 files changed

Lines changed: 198 additions & 51 deletions

File tree

Lines changed: 60 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,106 @@
11
"""
22
Dialog that allows user to specify a new config file section name.
33
Used to get new highlight theme and keybinding set names.
4+
The 'return value' for the dialog, used two placed in configDialog.py,
5+
is the .result attribute set in the Ok and Cancel methods.
46
"""
57
from tkinter import *
68
import tkinter.messagebox as tkMessageBox
79

810
class GetCfgSectionNameDialog(Toplevel):
9-
def __init__(self,parent,title,message,usedNames):
11+
def __init__(self, parent, title, message, used_names):
1012
"""
1113
message - string, informational message to display
12-
usedNames - list, list of names already in use for validity check
14+
used_names - string collection, names already in use for validity check
1315
"""
1416
Toplevel.__init__(self, parent)
1517
self.configure(borderwidth=5)
16-
self.resizable(height=FALSE,width=FALSE)
18+
self.resizable(height=FALSE, width=FALSE)
1719
self.title(title)
1820
self.transient(parent)
1921
self.grab_set()
2022
self.protocol("WM_DELETE_WINDOW", self.Cancel)
2123
self.parent = parent
22-
self.message=message
23-
self.usedNames=usedNames
24-
self.result=''
25-
self.CreateWidgets()
26-
self.withdraw() #hide while setting geometry
24+
self.message = message
25+
self.used_names = used_names
26+
self.create_widgets()
27+
self.withdraw() #hide while setting geometry
2728
self.update_idletasks()
2829
#needs to be done here so that the winfo_reqwidth is valid
2930
self.messageInfo.config(width=self.frameMain.winfo_reqwidth())
30-
self.geometry("+%d+%d" %
31-
((parent.winfo_rootx()+((parent.winfo_width()/2)
32-
-(self.winfo_reqwidth()/2)),
33-
parent.winfo_rooty()+((parent.winfo_height()/2)
34-
-(self.winfo_reqheight()/2)) )) ) #centre dialog over parent
35-
self.deiconify() #geometry set, unhide
31+
self.geometry(
32+
"+%d+%d" % (
33+
parent.winfo_rootx() +
34+
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
35+
parent.winfo_rooty() +
36+
(parent.winfo_height()/2 - self.winfo_reqheight()/2)
37+
) ) #centre dialog over parent
38+
self.deiconify() #geometry set, unhide
3639
self.wait_window()
3740

38-
def CreateWidgets(self):
39-
self.name=StringVar(self)
40-
self.fontSize=StringVar(self)
41-
self.frameMain = Frame(self,borderwidth=2,relief=SUNKEN)
42-
self.frameMain.pack(side=TOP,expand=TRUE,fill=BOTH)
43-
self.messageInfo=Message(self.frameMain,anchor=W,justify=LEFT,padx=5,pady=5,
44-
text=self.message)#,aspect=200)
45-
entryName=Entry(self.frameMain,textvariable=self.name,width=30)
41+
def create_widgets(self):
42+
self.name = StringVar(self.parent)
43+
self.fontSize = StringVar(self.parent)
44+
self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN)
45+
self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH)
46+
self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT,
47+
padx=5, pady=5, text=self.message) #,aspect=200)
48+
entryName = Entry(self.frameMain, textvariable=self.name, width=30)
4649
entryName.focus_set()
47-
self.messageInfo.pack(padx=5,pady=5)#,expand=TRUE,fill=BOTH)
48-
entryName.pack(padx=5,pady=5)
49-
frameButtons=Frame(self)
50-
frameButtons.pack(side=BOTTOM,fill=X)
51-
self.buttonOk = Button(frameButtons,text='Ok',
52-
width=8,command=self.Ok)
53-
self.buttonOk.grid(row=0,column=0,padx=5,pady=5)
54-
self.buttonCancel = Button(frameButtons,text='Cancel',
55-
width=8,command=self.Cancel)
56-
self.buttonCancel.grid(row=0,column=1,padx=5,pady=5)
50+
self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH)
51+
entryName.pack(padx=5, pady=5)
5752

58-
def NameOk(self):
59-
#simple validity check for a sensible
60-
#ConfigParser file section name
61-
nameOk=1
62-
name=self.name.get()
63-
name.strip()
53+
frameButtons = Frame(self, pady=2)
54+
frameButtons.pack(side=BOTTOM)
55+
self.buttonOk = Button(frameButtons, text='Ok',
56+
width=8, command=self.Ok)
57+
self.buttonOk.pack(side=LEFT, padx=5)
58+
self.buttonCancel = Button(frameButtons, text='Cancel',
59+
width=8, command=self.Cancel)
60+
self.buttonCancel.pack(side=RIGHT, padx=5)
61+
62+
def name_ok(self):
63+
''' After stripping entered name, check that it is a sensible
64+
ConfigParser file section name. Return it if it is, '' if not.
65+
'''
66+
name = self.name.get().strip()
6467
if not name: #no name specified
6568
tkMessageBox.showerror(title='Name Error',
6669
message='No name specified.', parent=self)
67-
nameOk=0
6870
elif len(name)>30: #name too long
6971
tkMessageBox.showerror(title='Name Error',
7072
message='Name too long. It should be no more than '+
7173
'30 characters.', parent=self)
72-
nameOk=0
73-
elif name in self.usedNames:
74+
name = ''
75+
elif name in self.used_names:
7476
tkMessageBox.showerror(title='Name Error',
7577
message='This name is already in use.', parent=self)
76-
nameOk=0
77-
return nameOk
78+
name = ''
79+
return name
7880

7981
def Ok(self, event=None):
80-
if self.NameOk():
81-
self.result=self.name.get().strip()
82+
name = self.name_ok()
83+
if name:
84+
self.result = name
8285
self.destroy()
8386

8487
def Cancel(self, event=None):
85-
self.result=''
88+
self.result = ''
8689
self.destroy()
8790

8891
if __name__ == '__main__':
89-
#test the dialog
90-
root=Tk()
92+
import unittest
93+
unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False)
94+
95+
# also human test the dialog
96+
root = Tk()
9197
def run():
92-
keySeq=''
9398
dlg=GetCfgSectionNameDialog(root,'Get Name',
94-
'The information here should need to be word wrapped. Test.')
99+
"After the text entered with [Ok] is stripped, <nothing>, "
100+
"'abc', or more that 30 chars are errors. "
101+
"Close with a valid entry (printed), [Cancel], or [X]",
102+
{'abc'})
95103
print(dlg.result)
96-
Button(root,text='Dialog',command=run).pack()
104+
Message(root, text='').pack() # will be needed for oher dialog tests
105+
Button(root, text='Click to begin dialog test', command=run).pack()
97106
root.mainloop()

Lib/idlelib/idle_test/mock_tk.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Classes that replace tkinter gui objects used by an object being tested.
2+
A gui object is anything with a master or parent paramenter, which is typically
3+
required in spite of what the doc strings say.
4+
"""
5+
6+
class Var:
7+
"Use for String/Int/BooleanVar: incomplete"
8+
def __init__(self, master=None, value=None, name=None):
9+
self.master = master
10+
self.value = value
11+
self.name = name
12+
def set(self, value):
13+
self.value = value
14+
def get(self):
15+
return self.value
16+
17+
class Mbox_func:
18+
"""Generic mock for messagebox functions. All have same call signature.
19+
Mbox instantiates once for each function. Tester uses attributes.
20+
"""
21+
def __init__(self):
22+
self.result = None # The return for all show funcs
23+
def __call__(self, title, message, *args, **kwds):
24+
# Save all args for possible examination by tester
25+
self.title = title
26+
self.message = message
27+
self.args = args
28+
self.kwds = kwds
29+
return self.result # Set by tester for ask functions
30+
31+
class Mbox:
32+
"""Mock for tkinter.messagebox with an Mbox_func for each function.
33+
This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x.
34+
Example usage in test_module.py for testing functios in module.py:
35+
---
36+
from idlelib.idle_test.mock_tk import Mbox
37+
import module
38+
39+
orig_mbox = module.tkMessageBox
40+
showerror = Mbox.showerror # example, for attribute access in test methods
41+
42+
class Test(unittest.TestCase):
43+
44+
@classmethod
45+
def setUpClass(cls):
46+
module.tkMessageBox = Mbox
47+
48+
@classmethod
49+
def tearDownClass(cls):
50+
module.tkMessageBox = orig_mbox
51+
---
52+
When tkMessageBox functions are the only gui making calls in a method,
53+
this replacement makes the method gui-free and unit-testable.
54+
For 'ask' functions, set func.result return before calling method.
55+
"""
56+
askokcancel = Mbox_func() # True or False
57+
askquestion = Mbox_func() # 'yes' or 'no'
58+
askretrycancel = Mbox_func() # True or False
59+
askyesno = Mbox_func() # True or False
60+
askyesnocancel = Mbox_func() # True, False, or None
61+
showerror = Mbox_func() # None
62+
showinfo = Mbox_func() # None
63+
showwarning = Mbox_func() # None
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Unit tests for idlelib.configSectionNameDialog"""
2+
import unittest
3+
from idlelib.idle_test.mock_tk import Var, Mbox
4+
from idlelib import configSectionNameDialog as name_dialog_module
5+
6+
name_dialog = name_dialog_module.GetCfgSectionNameDialog
7+
8+
class Dummy_name_dialog:
9+
# Mock for testing the following methods of name_dialog
10+
name_ok = name_dialog.name_ok
11+
Ok = name_dialog.Ok
12+
Cancel = name_dialog.Cancel
13+
# Attributes, constant or variable, needed for tests
14+
used_names = ['used']
15+
name = Var()
16+
result = None
17+
destroyed = False
18+
def destroy(self):
19+
self.destroyed = True
20+
21+
# name_ok calls Mbox.showerror if name is not ok
22+
orig_mbox = name_dialog_module.tkMessageBox
23+
showerror = Mbox.showerror
24+
25+
class TestConfigName(unittest.TestCase):
26+
dialog = Dummy_name_dialog()
27+
28+
@classmethod
29+
def setUpClass(cls):
30+
name_dialog_module.tkMessageBox = Mbox
31+
32+
@classmethod
33+
def tearDownClass(cls):
34+
name_dialog_module.tkMessageBox = orig_mbox
35+
36+
def test_blank_name(self):
37+
self.dialog.name.set(' ')
38+
self.assertEqual(self.dialog.name_ok(), '')
39+
self.assertEqual(showerror.title, 'Name Error')
40+
self.assertIn('No', showerror.message)
41+
42+
def test_used_name(self):
43+
self.dialog.name.set('used')
44+
self.assertEqual(self.dialog.name_ok(), '')
45+
self.assertEqual(showerror.title, 'Name Error')
46+
self.assertIn('use', showerror.message)
47+
48+
def test_long_name(self):
49+
self.dialog.name.set('good'*8)
50+
self.assertEqual(self.dialog.name_ok(), '')
51+
self.assertEqual(showerror.title, 'Name Error')
52+
self.assertIn('too long', showerror.message)
53+
54+
def test_good_name(self):
55+
self.dialog.name.set(' good ')
56+
showerror.title = 'No Error' # should not be called
57+
self.assertEqual(self.dialog.name_ok(), 'good')
58+
self.assertEqual(showerror.title, 'No Error')
59+
60+
def test_ok(self):
61+
self.dialog.destroyed = False
62+
self.dialog.name.set('good')
63+
self.dialog.Ok()
64+
self.assertEqual(self.dialog.result, 'good')
65+
self.assertTrue(self.dialog.destroyed)
66+
67+
def test_cancel(self):
68+
self.dialog.destroyed = False
69+
self.dialog.Cancel()
70+
self.assertEqual(self.dialog.result, '')
71+
self.assertTrue(self.dialog.destroyed)
72+
73+
74+
if __name__ == '__main__':
75+
unittest.main(verbosity=2, exit=False)

0 commit comments

Comments
 (0)