In this chapter, we explore how to generate tests for Graphical User Interfaces (GUIs), abstracting from our previous examples on Web testing. Building on general means to extract user interface elements and activate them, our techniques generalize to arbitrary graphical user interfaces, from rich Web applications to mobile apps, and systematically explore user interfaces through forms and navigation elements.
In this chapter, we therefore take a different approach to user interface testing. Rather than using HTTP and HTML as the mechanisms for interaction, we leverage a dedicated UI testing framework, which allows us to
Although we will again illustrate our approach using a Web server, the approach easily generalizes to arbitrary user interfaces. In fact, the UI testing framework we use, Selenium, also comes in variants that run for Android apps.
# ignore if 'CI' in os.environ: # Can't run this in our continuous environment, # since it can't run a headless Web browser sys.exit(0)
db = init_db()
This is the address of our web server:
httpd_process, httpd_url = start_httpd() print_url(httpd_url)
webbrowser(), we can retrieve the HTML of the home page, and use
HTML() to render it.
127.0.0.1 - - [12/Nov/2023 13:52:10] "GET / HTTP/1.1" 200 -
Selenium is a framework for testing Web applications by automating interaction in the browser. Selenium provides an API that allows one to launch a Web browser, query the state of the user interface, and interact with individual user interface elements. The Selenium API is available in a number of languages; we use the Selenium API for Python.
A Selenium web driver is the interface between a program and a browser controlled by the program. The following code starts a Web browser in the background, which we then control through the web driver.
We support both Firefox and Google Chrome.
BROWSER = 'firefox' # Set to 'chrome' if you prefer Chrome
if BROWSER == 'firefox': assert shutil.which('geckodriver') is not None, \ "Please install the 'geckodriver' executable " \ "from https://github.com/mozilla/geckodriver/releases"
if BROWSER == 'chrome': assert shutil.which('chromedriver') is not None, \ "Please install the 'chromedriver' executable " \ "from https://chromedriver.chromium.org"
The browser is headless, meaning that it does not show on the screen.
HEADLESS = True
Note: If the notebook server runs locally (i.e. on the same machine on which you are seeing this), you can also set
False and see what happens right on the screen as you execute the notebook cells. This is very much recommended for interactive sessions.
def start_webdriver(browser=BROWSER, headless=HEADLESS, zoom=1.4): # Set headless option if browser == 'firefox': options = webdriver.FirefoxOptions() if headless: # See https://www.browserstack.com/guide/firefox-headless options.add_argument("--headless") elif browser == 'chrome': options = webdriver.ChromeOptions() if headless: # See https://www.selenium.dev/blog/2023/headless-is-going-away/ options.add_argument("--headless=new") else: assert False, "Select 'firefox' or 'chrome' as browser" # Start the browser, and obtain a _web driver_ object such that we can interact with it. if browser == 'firefox': # For firefox, set a higher resolution for our screenshots options.set_preference("layout.css.devPixelsPerPx", repr(zoom)) gui_driver = webdriver.Firefox(options=options) # We set the window size such that it fits our order form exactly; # this is useful for not wasting too much space when taking screen shots. gui_driver.set_window_size(700, 300) elif browser == 'chrome': gui_driver = webdriver.Chrome(options=options) gui_driver.set_window_size(700, 210 if headless else 340) return gui_driver
gui_driver = start_webdriver(browser=BROWSER, headless=HEADLESS)
We can now interact with the browser programmatically. First, we have it navigate to the URL of our Web server:
We see that the home page is actually accessed, together with a (failing) request to get a page icon:
127.0.0.1 - - [12/Nov/2023 13:52:12] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [12/Nov/2023 13:52:12] "GET /favicon.ico HTTP/1.1" 404 -
To see what the "headless" browser displays, we can obtain a screenshot. We see that it actually displays the home page.
To interact with the Web page through Selenium and the browser, we can query Selenium for individual elements. For instance, we can access the UI element whose
name attribute (as defined in HTML) is
name = gui_driver.find_element(By.NAME, "name")
Once we have an element, we can interact with it. Since
name is a text field, we can send it a string using the
send_keys() method; the string will be translated into appropriate keystrokes.
In the screenshot, we can see that the
name field is now filled:
Similarly, we can fill out the email, city, and ZIP fields:
email = gui_driver.find_element(By.NAME, "email") email.send_keys("firstname.lastname@example.org")
city = gui_driver.find_element(By.NAME, 'city') city.send_keys("Seattle")
zip = gui_driver.find_element(By.NAME, 'zip') zip.send_keys("98104")
The check box for terms and conditions is not filled out, but clicked instead using the
terms = gui_driver.find_element(By.NAME, 'terms') terms.click()
The form is now fully filled out. By clicking on the
submit button, we can place the order:
submit = gui_driver.find_element(By.NAME, 'submit') submit.click()
We see that the order is being processed, and that the Web browser has switched to the confirmation page.
127.0.0.1 - - [12/Nov/2023 13:52:12] INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'email@example.com', 'Seattle', '98104')
127.0.0.1 - - [12/Nov/2023 13:52:12] "GET /order?item=tshirt&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=98104&terms=on&submit=Place+order HTTP/1.1" 200 -
We can query the web driver for all elements of a particular type. Querying for HTML anchor elements (
<a>) for instance, gives us all links on a page.
links = gui_driver.find_elements(By.TAG_NAME, "a")
We can query the attributes of UI elements – for instance, the URL the first anchor on the page links to:
What happens if we click on it? Very simple: We switch to the Web page being referenced.
127.0.0.1 - - [12/Nov/2023 13:52:12] "GET /terms HTTP/1.1" 200 -