How to test a function that is using user-interface - python-3.x

I am trying to test a function that needs an interaction of the user. The question is how can I do it programmatically ?
Here is an example where we ask for the user to select an item in a list (main.py) :
import tkinter as tk
from tkinter import Button, OptionMenu, StringVar
def ask_for_item_in_list(lst, title, default_index=0):
root, item = tk.Tk(), None
WIDTH, HEIGHT = 300, 120
root.title(title)
root.maxsize(width=WIDTH, height=HEIGHT)
root.minsize(width=WIDTH, height=HEIGHT)
root.resizable(0, 0)
variable = StringVar(root)
variable.set(lst[default_index])
option_menu = OptionMenu(root, variable, *lst)
option_menu.pack(fill="none", expand=True)
def on_close():
# The window has been closed by the user
variable.set(None)
close()
def close():
# It quits mainloop()
root.quit()
# It closes the window
root.destroy()
button_ok = Button(root, text='OK', command=close)
button_ok.pack(fill='none', expand=True)
root.protocol('WM_DELETE_WINDOW', on_close)
# Execution stops here as long as the user has not closed the window or
# pressed ok
root.mainloop()
# We retrieve the selected item
item = variable.get()
if item == 'None':
item = None
return item
if __name__ == '__main__':
lst = ['Item 1', 'Item 2', 'Item 3']
title = 'Select an item'
default_selected_idx = lst.index('Item 2')
selected_item = ask_for_item_in_list(lst, title, default_selected_idx)
print(selected_item)
I used pytest to write all my tests since I can't use object oriented programming. Actually, the code must be maintainable by people who are not professional developpers.
As you can see, I can't test this function this way, since it will wait for the user input (test_main.py):
from main import ask_for_item_in_list
def test_ask_for_item_in_list():
lst = ['Item 1', 'Item 2', 'Item 3']
title = 'Select an item'
# Here TRY to test if changing the default selected index works
default_selected_idx = lst.index('Item 2')
# Code to simualte that the user as clicked on OK ?
# user.click_button('OK') ?
selected_item = ask_for_item_in_list(lst, title, default_selected_idx)
assert selected_item == 'Item 2'
Do I need to change the way I have coded this ?
Is it relevant to test this kind of function ?
I have faced this problem many times (whatever is the language used), I would like to know how this is supposed to be done in a clean way.
Thanks for reading ! :)

Usually one fills the user input beforehand with expected or special values and then calls the test function several times. Also you may simulate clicks with various tools.
In C++ you could do something like:
int number_of_tests = 10;
int tests_passed = 0;
tests_passed += my_test_function_int(0);
tests_passed += my_test_function_int(-1);
...
tests_passed += my_test_function_string("foo");
tests_passed += my_test_function_string("");
tests_passed += my_test_function_string(" ");
...
return (tests_passed == number_of_tests);
This is just an example how one can do it (in our company we do it this way).
Also it is not very hard to add new tests for non-programmers or new people.

Related

How to handle variables of multiple check buttons inside a menu in Tkinter?

I wanted to make a multi-selection dropdown list in Tkinter so browsing online I came to the solution of using add_checkbuttons into a menu. The solution it's working, at least graphically, but now I want the GUI to actually do something when a check is marked. I tried with a simple function that print the value as a command for each check button but it's only being called once.
Here's my code so far:
root = tk.Tk()
groceries = ['Apple', 'Banana', 'Carrot']
def print_groceries(bucket,item):
print(bucket[item].get())
menubar = tk.Menu(root)
viewMenu = tk.Menu(menubar, tearoff = 0)
bucket={}
for item in groceries:
bucket[item] = tk.BooleanVar(False)
viewMenu.add_checkbutton(label = item, variable = bucket[item], command=print_groceries(bucket,item))
menubar.add_cascade(menu = viewMenu, label = "Buy")
root.config(menu = menubar)
root.mainloop()
You have assigned the return value of print_groceries to the command parameter. Therefore, when your code is executed, print_groceries is executed thrice at the time of defining the checkbuttons. As all the checkbuttons are unchecked initially, you see that False is printed thrice.
Solution:
The command parameter takes a function, not the value returned by a function (unless the return value is itself a function).
To create a simple function, you can use lambda as shown below:
lambda <parameter-1>,<parameter-2>,... : <task-to-perform>
Also, you must use lambda bucket=bucket, item=item: print_groceries(bucket,item) and not lambda : print_groceries(bucket,item). You can find the reason for the same in this answer.
Working Code:
import tkinter as tk
root = tk.Tk()
groceries = ['Apple', 'Banana', 'Carrot']
def print_groceries(bucket,item):
print(bucket[item].get())
menubar = tk.Menu(root)
viewMenu = tk.Menu(menubar, tearoff = 0)
bucket={}
for item in groceries:
bucket[item] = tk.BooleanVar(False)
func = lambda bucket=bucket, item=item: print_groceries(bucket,item)
viewMenu.add_checkbutton(label = item, variable = bucket[item], command = func)
menubar.add_cascade(menu = viewMenu, label = "Buy")
root.config(menu = menubar)
root.mainloop()

How to store in variables current row selection within tree widget to query a database using python

I wonder if it is possible to store in variables the contents from a tree widget row (when it is selected with the mouse) see picture. Basically I want to sync my tree with a database, every time when I insert or delete an element in my tree, my database needs to auto update.
With the insert part it is not a problem , because I have entry widgets, but I don't know how to manage the delete part. Therefore, I wonder if it is possible to do this with some cursor selection function.
I have been trying for a very long time to find a solution for this, I would really appreciate if someone can help me with some hints
Code:
import tkinter
from tkinter import ttk
class cards(tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self, parent)
self.parent=parent
self.parent.geometry("800x500")
self.initialize_user_interface()
def initialize_user_interface(self):
self.parent.title("cards")
self.parent.grid_rowconfigure(0,weight=1)
self.parent.grid_columnconfigure(0,weight=1)
self.parent.config(background="lavender")
self.Card_label = tkinter.Label(self.parent, text = "Card type:")
self.Card_entry = tkinter.Entry(self.parent)
self.Card_label.place(x=5,y=5)
self.Card_entry.place(x=70,y=5)
self.SN_label = tkinter.Label(self.parent, text = "SN:")
self.SN_entry = tkinter.Entry(self.parent)
self.SN_label.place(x=5,y=40)
self.SN_entry.place(x=70,y=40)
self.submit_button = tkinter.Button(self.parent, text = "Insert", command = self.insert_data)
self.submit_button.place(x=210,y=15)
self.exit_button = tkinter.Button(self.parent, text = "Exit", command = self.exit)
self.exit_button.place(x=270,y=15)
self.tree = ttk.Treeview( self.parent, columns=('Card Type', 'SN'))
self.tree.heading('#0', text='Nr.')
self.tree.heading('#1', text='Card Type')
self.tree.heading('#2', text='SN')
self.tree.column('#1', stretch=tkinter.YES)
self.tree.column('#2', stretch=tkinter.YES)
self.tree.column('#0', stretch=tkinter.YES)
self.tree.place(x=0,y=100)
self.treeview = self.tree
self.i = 1
def exit(self):
self.master.destroy()
def insert_data(self):
self.treeview.insert('', 'end', text=str(self.i), values=(self.Card_entry.get(), self.SN_entry.get()))
self.i = self.i + 1
def main():
root=tkinter.Tk()
d=cards(root)
root.mainloop()
if __name__=="__main__":
main()
You can use
for item in self.tree.selection():
print(self.tree.item(item, "text"))
print(self.tree.item(item, "values"))
#print(self.tree.item(item))
to see data from all selected rows - you can select more than one row.
You can use it in function assigned to button
or you can use bind() to assign function to mouse click on row.

Trying to build Risk style game in Tkinter

I have been trying to build my skills in Python and am trying to create a risk style game.
I am not far in to it at the moment as I am trying to get to grips with classes and Tkinter.
My first trial is to create a series of buttons to take the place of the different countries. I then want these buttons to update the amount of armies on the country when they are clicked.
So far I have been able to get the map to generate from the class I have created and the buttons are clickable. When a button is clicked it updates the amount of armies but always for the last button.
How do I get it so that the button I click updates and not the last one?
Have I gone about this in entirely the wrong way?
from tkinter import *
import random
class territory:
def __init__ (self, country, player = "1", current_armies = 0, x=0, y=0):
self.country = country
self.current_armies = current_armies
self.player = player
self.y = y
self.x = x
def get_armies(self):
print(self.country + " has " + str( self.current_armies)+ " armies.")
def add_armies (self, armies):
self.current_armies += armies
def roll_dice (self, dice=1):
rolls = []
for i in range(0, dice):
rolls.append(random.randint(1,6))
rolls.sort()
rolls.reverse()
print (self.country + " has rolled " + str(rolls))
return rolls
def owner(self):
print (self.country + " is owned by " + self.player)
def get_country(self):
print(country)
def button (self):
Button(window, text = territories[0].current_armies, width = 10, command = click1(territories, 0)).grid(row=y,column=x)
window = Tk()
def create_territories():
countries = ["UK", "GER", "SPA", "RUS"]
terr_pos = [[1,0],[2,0],[1,5],[4,1]]
sta_arm = [1,1,1,1]
terr = []
player = "1"
for i in range(len(countries)):
terr.append(territory(countries[i],player, sta_arm [i] , terr_pos[i][0],terr_pos[i][1]))
if player == "1":
player = "2"
else:
player = "1"
return terr
def click1(territory, i):
territory[i].current_armies += 1
build_board(territory)
def build_board(territories):
for i in range(0,4):
Button(window, text = territories[i].country+"\n"+str(territories[i].current_armies), width = 10, command = lambda: click1(territories, i)).grid(row=territories[i].y,column=territories[i].x)
territories = create_territories()
window.title ("Domination")
create_territories()
build_board(territories)
window.mainloop()
In your def button(self):... you are always referencing territories[0]:
Button(window, text=territories[0].current_armies,... command=click1(territories, 0)...
As such, you are always using the first territory as your reference, so you ought to initialize each territory with its index in territories[] so you can pass that into your Button constructor.
On your question of "entirely the wrong way," I'd personally send that question over to CodeReview, since that's more of their domain (we fix broken code, they address smelly code), though there is significant overlap. We do prefer one question per question, however, and "is this whole thing wrong?" is a little broad for StackOverflow.

calling a procedure when a button is pressed

i thought this would be easy but here i am!!
I was to call a procedure when the button is pressed and display the result on a label
class DSFRSapp(App):
def build(self):
self.root = FloatLayout()
i = Image(source='DSFRSLogo.png',
allow_stretch=True,
pos_hint = ({'center_x':0.5, 'y': .25}))
spinner = Spinner(
text='Pick a Station',
values=('Appledore','Axminster','Bampton','Barnstaple','Bere Alston','Bideford','Bovey Tracey','Braunton','Bridgwater','Brixham','Buckfastleigh','Budleigh Salterton','Burnham on sea','Camels Head','Castle Cary','Chagford','Chard','Cheddar','Chulmleigh','Colyton','Combe Martin','Crediton','Crewkerne','Crownhill','Cullompton','Dartmouth','Dawlish','Exeter Danes Castle','Exeter Middlemoor','Exmouth','Frome','Glastonbury','Greenbank','Hartland','Hatherleigh','Holsworthy','Honiton','Ilfracombe','Ilminster','Ivybridge','Kingsbridge','Kingston','Lundy Island','Lynton','Martock','Minehead','Modbury','Moretonhampstead','Nether Stowey','Newton Abbot','North Tawton','Okehampton','Ottery St Mary','Paignton','Plympton','Plymstock','Porlock','Princetown','Salcombe','Seaton','Shepton Mallet','Sidmouth','Somerton','South Molton','Street','Taunton','Tavistock','Teignmouth','Tiverton','Topsham','Torquay','Torrington','Totnes','USAR','Wellington','Wells','Williton','Wincanton','Witheridge','Wiveliscombe','Woolacombe','Yelverton','Yeovil'),
size_hint=(None, None),
size=(150, 44),
pos_hint = ({'center_x':0.5, 'y': 0.35}))
b = Button(text="Search For Incidents",size_hint=(None, None),
pos_hint =({'center_x':0.5, 'y': 0.25}),
size=(150, 44))
LblRes = Label(text="Results will display here",
pos_hint =({'center_x':0.5, 'y': 0.15}),
size_hint=(600,100),color=(1,1,1,1),font_size=35)
b.bind(on_press=FindIncident(Spinner.text))
self.root.add_widget(spinner)
self.root.add_widget(LblRes)
self.root.add_widget(i)
self.root.add_widget(b)
return
def FindIncident( sStation ):
webpage = request.urlopen("http://www.dsfire.gov.uk/News/Newsdesk/IncidentsPast7days.cfm?siteCategoryId=3&T1ID=26&T2ID=35")#main page
soup = BeautifulSoup(webpage)
incidents = soup.find(id="CollapsiblePanel1") #gets todays incidents panel
Links = [] #create list call Links
for line in incidents.find_all('a'): #get all hyperlinks
Links.append("http://www.dsfire.gov.uk/News/Newsdesk/"+line.get('href')) #loads links into Links list while making them full links
n = 0
e = len(Links)
if e == n: #if no links available no need to continue
print("No Incidents Found Please Try Later")
sys.exit(0)
sFound = False
while n < e: #loop through links to find station
if sFound: #if the station has been found stop looking
sys.exit(0)
webpage = request.urlopen(Links[n]) #opens link in list)
soup = BeautifulSoup(webpage) #loads webpage
if soup.find_all('p', text=re.compile(r'{}'.format(sStation))) == []:#check if returned value is found
#do nothing leaving blank gave error
a = "1" #this is pointless but stops the error
else:
print(soup.find_all('p', text=re.compile(r'{}'.format(sStation)))) #output result
WebLink = Links[n]
sFound = True # to avoid un needed goes through the loop process
n=n+1 # moves counter to next in list
if not sFound: #after looping process if nothing has been found output nothing found
print("nothing found please try again later")
return;
if __name__ =="__main__":
DSFRSapp().run()
so when the button is pressed to call FindIncident using the spinner text as the string. That way it can search for the station and instead of print i will put the results in the label, possibly with a link to the website too.
any help would be great!
Raif
b.bind(on_press=FindIncident(Spinner.text))
You need to pass a function as the argument to bind. You aren't passing FindIncident, you're calling it and passing the result...which is None.
Try
from functools import partial
b.bind(on_press=partial(FindIncident, spinner.text))
Also declare FindIncident as def FindIncident( sStation, *args ):, as bind will automatically pass extra arguments. You could also do the same thing with a lambda function.
Be careful with your code cases as well - at the moment you use Spinner.text, when you probably mean spinner.text, as you created a variable spinner.

Multithreading in Tkinter

I am new to Python and Tkinter but I was wondering if I could do Multithreading in Tkinter.
I have a Restaurant Simulation program and whenever it accepts an order, this is what should happen:
1) Timer that counts down and shows how many seconds are left before the order is done
2) While the Timer is counting, I want to create another instance of the Restaurant Simulation so that it could accept another order.
This is what I have tried:
from Tkinter import *
class food():
def __init__(self):
self.inventory = []
def cookOrder(self,type):
if type is 'AA': #Cook Barf
self.inventory[0]+=1
class Restaurant():
def Callback(self,root):
if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
root.destroy()
def MainMenu(self):
self.Main = Tk()
self.Main.grid()
Title = Label(self.Main,text = "Welcome to Resto!",font=("Bauhaus 93",20))
Title.grid(columnspan=6)
RestoMenu = Button(self.Main,text = "Order Food",command = lambda:self.Restaurant_Menu('AA'),font=("High Tower Text",12))
RestoMenu.grid(row=2,column=0)
self.Main.mainloop()
def Restaurant_Menu(self,type):
self.Main.destroy()
self.setCookTime(type)
def setCookTime(self,type):
self.MainMenu() #This is not working as I planned it to be
self.cookTimer = Tk()
self.timeLabel = Label(text="")
self.timeLabel.pack()
if type is "AA":
self.Tick(10,type)
self.cookTimer.mainloop()
self.cookTimer.wm_protocol ("WM_DELETE_WINDOW", self.Callback(self.cookTimer)) #Getting TCL Error
def Tick(self,time,type):
time -=1
if time ==-1:
food.cookOrder(type)
self.cook.destroy()
else:
self.timeLabel.configure(text="%d" %time)
self.cookTimer.after(1000, lambda: self.Tick(time,type))
Food = food()
Resto = Restaurant()
Resto.MainMenu()
Now, the problem with the code is, it is not creating another instance of the Restaurant while the countdown is going on. Any help?
Plus, how do you ask the user for the confirmation of exiting program when he clicks the exit button while the timer is ticking, using WM_DELETE_WINDOW?

Resources