Pages

Thursday, 28 April 2022

Let the computer test your Python GUI application

Let the computer test your Python GUI application

In this article you’ll see how easy it is to write automated tests for Graphical User Interfaces (GUIs) written using the brilliant guizero Python library.

I’ll start with a horror story which explains why I’m so keen on automated GUI tests.

Next I’ll describe an application that I’m using as an example. The code for the app and the test are available on GitHub; the link is in the resources section at the end of this post.

After that, I’ll show how the tests are built up and describe how they enabled me to find and fix a bug.

A personal horror story

A couple of years ago I presented a Quiz Runner application to an audience of Digital Makers.

The Quiz Runner application used a workstation to manage the Quiz.

Quiz Contestants used micro:bits to ‘buzz’ in when they thought they knew an answer.

The micro:bits communicated via radio using the micro:bit’s built-in radio capability, and everything (workstation and micro:bits) was programmed in Python.

The Quiz Runner application had a simple Graphical User Interface (GUI) to control the quiz and keep scores, and a micro:bit connected via its serial interface to interact with the contestants’ micro:bits.

The demo started really well. Then something went wrong with the GUI and I had to abandon the demo. I was annoyed and embarrassed.

Software craftsmanship

My grandfather was a carpenter, as were his father and Grandfather. They were craftsmen in wood. I like to think of myself as a craftsman in software, but I felt I’d just made a door that would not open.

When I had time to explore the problem I found it very hard to reproduce. I needed to hop between the QuizRunner App and the four team micro:bits, clicking and pressing the right things at the right time for dozens of permutations of behaviour.

I gave up.

The first time that you manually test a GUI application, it feels like fun.

The tenth time? Not so much.

The downsides of manual testing

Manual testing has its place, but it can be boring and error-prone. Worse still, there’s no automatic record of what was tested, or what worked.

Because it’s boring, many developers avoid it as far as possible. That can mean that edge cases get tested in QA or production rather than in development. That’s expensive - the later a bug is detected, the greater the cost of fixing it.

So how can you create automated tests for gui-based applications?

How can you use automated tests with GUIs?

There are gui-testing libraries available, but the commercial products are expensive and most of the open-source tools I’ve found are cumbersome.

There is good news, though, if you use Python’s excellent guizero library.

guizero was written by Laura Sach and Martin O’Hanlon.

They are experienced educators, and they work for the Raspberry Pi Foundation.

guizero is easy to use, it has great documentation and there’s a super Book of Examples!

The book is called Create Graphical User Interfaces with Python, and it’s available from The MagPi website.

I’m a big fan of guizero. It ticks all the boxes in my Python library checklist, and I use it a lot. The library has lots of automated tests, but the book is aimed at beginners, so it recommends manual testing.

To keep the code simple, the book also makes use of global variables. I’m happy with that in a book for beginners, but experienced software developers try to avoid globals in their code.

I wondered how easy it would be for me to refactor to eliminate the globals, and to remove some code duplication.

Refactoring the original code

Refactoring is a technique that you can use to improve the design of existing code without its external behaviour.

You may have come across Martin Fowler’s book on the subject. It’s a classic, and I refer to it a lot.

I refactored one of my favourite examples from Create Graphical User Interfaces with Python. It’s a game of Noughts and Crosses (or Tic-tac-toe if you’re reading this in North America).

I ended up with code that had no globals and tests that exercised the system thoroughly.

How do the tests work?

Add the magic code that pushes buttons

The most important code is this short fragment:

from guizero import PushButton

def push(button: PushButton) -> None:
    button.tk.invoke()
It allows you to write code in your test that has the same effect ass a user pressing a button in  the GUI.

I found it buried in the unit tests for the guizero library.

Set up the test fixtures

You create unit tests by writing Test Cases.

You set up the environment for your tests by creating test fixtures.

Opening a GUI application takes time, so you want to do it once per Test Case.

You do that by writing a class method called setUpClass.

import unittest
from tictactoe import TicTacToeApp

class TicTacToeTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.app = TicTacToeApp()

You write individual tests by creating Test Case methods whose names start with test.

The TestCase will run these in random order, so you need to make sure that your tests don’t interfere with each other.

You do that by writing a setUp method which will reset the game before each test method is run.

def setUp(self) -> None:
    self.app.reset_board()

This calls the reset_board method in the application:

def reset_board(self):
    for x in range(3):
        for y in range(3):
            self.square(x, y).text = " "
            self.square(x, y).enable()
    self.winner = None
    self.current_player = 'X'
    self.message.value = 'It is your turn, X'

Write the tests

Next you write tests to check that the game is working correctly.

Each test simulates a player making a move by clicking on a free cell on the board.

The tests also check whose turn it is before making the move.

The tests use a couple of helper methods to make the tests more readable.

There’s an excellent discussion of test readability in Clean Code. (See the resources at the end of the article.)

Use helper methods to make tests more readable

Here are the helper methods:

def message_value(self):
    return self.app.message.value

def play(self, x, y, player):
    self.assertEqual(self.message_value(), 'It is your turn, %s' % player)
    self.push(x, y)

The message_value method is just a concise way of finding the text of the last message sent by the game.

The play method checks that the last message tells the current player it’s their turn to play, and then clicks on the button that is specified by the x and y coordinates.

Write the first test

The first test just checks that the player changes after a move.

def test_turn_changes_after_player_moves(self):
    self.play(0, 0, 'X')
    self.assertEqual(self.message_value(), 'It is your turn, O')

That test passes. That’s good news. It tells you that the refactoring hasn’t broken that behaviour.

Test a game that X wins

Next write a test to check that the game knows when X has won.

def test_knows_if_x_has_won(self):
    self.play(0, 0, 'X')
    self.play(0, 1, 'O')
    self.play(1, 0, 'X')
    self.play(0, 2, 'O')
    self.play(2, 0, 'X')
    self.assertEqual(self.message_value(), 'X wins!')

That passes. You’re on a roll!

Test a win for O

Here’s a game that O wins.

def test_knows_if_o_has_won(self):
    self.play(0, 0, 'X')
    self.play(0, 1, 'O')
    self.play(1, 0, 'X')
    self.play(1, 1, 'O')
    self.play(1, 2, 'X')
    self.play(2, 1, 'O')
    self.assertEqual(self.message_value(), 'O wins!')

Check for a drawn game

If the last square is filled without either player winning, the game is drawn.

Here’s a test for that:

def test_recognises_draw(self):
    self.play(0, 0, 'X')
    self.play(1, 1, 'O')
    self.play(2, 2, 'X')
    self.play(0, 1, 'O')
    self.play(2, 1, 'X')
    self.play(2, 0, 'O')
    self.play(0, 2, 'X')
    self.play(1, 2, 'O')
    self.play(1, 0, 'X')
    self.assertEqual("It's a draw", self.message_value())

So far so good. But…

Finding and fixing a bug

When I was writing one of the tests I saw some strange behaviour. When I played the original version of the game I confirmed that it has a bug.

You can carry on making moves after the game has been won!

When you find a bug, you need to do four things.

  1. Write a test that demonstrates the bug by failing.
  2. Fix the bug
  3. Verify that the test now passes
  4. Check in your code!

Verify the bug

Here’s the test that demonstrates the bug. When you run it on an unfixed application it fails.

def test_game_stops_when_someone_wins(self):
    self.play(0, 0, 'X')
    self.play(0, 1, 'O')
    self.play(1, 0, 'X')
    self.play(1, 1, 'O')
    self.play(1, 2, 'X')
    self.play(2, 1, 'O')
    # O wins!
    self.push(0, 2) # should be ignored
    self.push(2, 0) # should be ignored
    self.push(2, 2) # should be ignored
    self.assertEqual(self.message_value(), 'O wins!')

Fix the bug

Here’s the application code that fixes the bug:

def disable_all_squares(self):
    for i in range(3):
        for j in range(3):
            self.square(i, j).disable()

The application needs to invoke that method when a game has been won.

Verify the bug is fixed

If you now run the tests they all pass, so it’s safe to check in your changes.

Success! You now have a working, tested application.

Resources

The code for this article is on GitHub.

guizero is available on GitHub.

You can install it via pip.

pip3 install guizero

Documentation is available here.

The book ‘Create Graphical User Interfaces with Python’ is available from the MagPi website.

I mentioned two other books:

Refactoring by Martin Fowler, and Clean Code by Robert C. Martin.

Questions? Ask in a comment, or tweet me at @RAREblog.

Image credits:

Micro:bit images courtesy of https://microbit.org Radio beacon: https://en.wikipedia.org/wiki/File:Wireless_tower.svg

Monday, 25 April 2022

Choosing a Python library

You’re working on a Python project, and you realise the next thing to do is a bit tricky. You don’t want to reinvent the wheel if you don’t have to. You wonder: has someone solved this problem before?

The first place to look is the Python Standard Library. One of Python’s great strengths is that it comes with batteries included; there are well-documented, tried and tested libraries to do all sorts of useful things.

No luck? Turn to GitHub for help - it usually can! Most of the libraries I use are hosted on GitHub.

Sometimes you’ll just find one candidate library; sometimes there will be more than one. You’ll need to decide if any fit the bill, and which looks best.

As we’ll see, GitHub can tell you a lot about the quality of the project.

Here are the things I like to ask about a library I’m considering. I’ve illustrated the checklist using the guizero project as an example, since I use it a lot and it ticks all the boxes.

Does it have good documentation?

Is the intended use clear, and does it match your requirements?

I’ve sometimes been caught by a library that looks as if it does the job but actually does something different.

And if it says it’s doing what you want, do the docs show you how to install and use the library?

Is the project active and well supported?

GitHub is your friend here. It’s easy to check how “alive” the project is, right from the project’s home page. You can use it to find out

  • When was the last update?
  • How many issues are there?
    • How many unresolved issues are there?
    • How long have they been around?
    • Are pull requests dealt with quickly?
  • How popular is the project?
    • How many people are watching it?
    • Is it often starred?
    • Has it often been forked?
      • Forking is a process for creating a personal copy which you can use for modification, or to suggest fixes or improvements via pull requests.

What is the quality of the code?

Does it support Python 3? Most libraries do these days, but if it doesn’t that is a show stopper.

Is the code readable? I look for good naming of functions, classes, methods, and variables, and I like the use of type hints where these help.

Is the code sufficiently commented?

Is it well structured: simple and short?

Is it sufficiently performant? This may not matter, but if it does, it might matter a lot.

Does it have appropriate licensing?

In my case I look for a licence that's compatible with the MIT licence but your choice will depend on your intended use and your context. If you’re a solo developer you will be able to decide for yourself but in some situations you may need to check Corporate Policy, and may even need to talk to your employer’s legal department.

Does it have a large, supportive community?

If the library has a Slack or Discord channel you may be able to dip in and quickly get the feel of the community. Is it friendly, respectful and helpful?

Is the library easy to learn?

Are there links to Tutorials, Books or Courses? Ideally you should be able to see how to get going straight from the README. (There is a README, isn’t there?)

Is it written by authors I know and trust?

That’s not essential, but it’s very reassuring.

Does it have a sensible, consistent API?

A good API satisfies the principle of least surprise: if you guess how to use it you’ll probably be right.

Does the author specifically encourage pull requests to add features or fix bugs?

Check the language of the documentation (especially the README) to see if the author wants to hear if you find a problem. It often indicates their mindset when writing it - do they intend for it to be used in a collaborative fashion or is it a “one man show”?

Are there automated tests?

I can write very simple test-free code, but as soon as things get at all complicated I know I need tests to keep me on track. If I am using other people’s code, plenty of automated tests reassure me that the code is likely to work. They also indicate that I can refactor the code or extend it safely if I need to.

Summary

A few minute’s search on GitHub can tell you a lot about whether you should use a 3rd party Python Library.

I hope you find this checklist helpful, and I welcome constructive feedback.

Thanks to Ben Nutall (@ben_nuttall), Michael Horne (@recantha) and @BrianLinuxing for their helpful suggestions.