satin-python is now available at PyPi

PyPi is a Python package index (also known as a cheeseshop). Satin is a UI testing library that I have been tinkering with on and off while developing Herculeum. I have now released version 0.1.0 of Satin in PyPi, in order to make it easier for others to download it.

The change is reflected in requirements-dev file of Herculeum. Now it is possible to download almost all dependencies of the game in just a single command:


pypi install -r requirements-dev

Satin and Qt event loop

Qt comes with a really good testing library called QTest. It can be used to simulate keyboard and mouse input when testing the system. PyQt naturally includes the same library, but using it can be a bit tricky sometimes. Mouse input works quite fine, but keyboard input does not work well without event loop running in the background. It is possible to get around this, by instantiating QApplication and calling exec_ – method on it. At this point QApplication takes charge of things and execution leaves the test method.

There are several ways around this. One is to construct two threads, using one to send keyboard and mouse events to the system under test. One should note though, that the QApplication should be running in the main thread (it will helpfully notify you if this is not the case). Another thing is shared resources. Constructing QPixmap objects outside of the thread running QApplication is not advisable (you’ll be notified about this too).

Second option is to construct a QTimer, that will start execution of your test code. In this model you do not need to worry about multiple threads or shared resources.

Doing either one  can get tedious, especially if the amount of tests is more than two. Satin now has a class decorator satin_suite, that will take care of the basic steps, leaving the developer free to write more expressive tests. Essentially, the decorator will perform following steps:

  1. Replace setup-function with a version that creates instance of QApplication and calls the original setup-function.
  2. Replace teardown-function with a version that deallocates QApplication and calls the original teardown-function.
  3. Replace each test_* function with a version that will install a QTimer, start QApplication, execute test code and exit QApplication

Details are still rough and the system has some faults (like any exception or failure halting the execution of tests).

Finding sub-widget with Satin

Satin has now ability to find a specific sub widget in widget hierarchy. Following is an example from herculeum:

def slot_with_item(name):
    """
    Create function to determine if given QWidget has an item with
    specified name

    :param name: name of item to detect
    :type name: string
    :returns: function to check if item is found or not
    :rtype: function
    """
    def matcher(widget):
        """
        Check if widget contains item with given name

        :param widget: widget to check
        :type widget: ItemGlyph
        :returns: True if name matches, otherwise false
        :rtype: boolean
        """
        if (widget != None
                and hasattr(widget, 'item')
                and widget.item != None
                and widget.item.name == name):
                    return True
        else:
            return False

    return matcher

    ...
    
    def test_dropping_item(self):
        """
        Test that item can be dropped
        """
        item = (ItemBuilder()
                    .with_name('dagger')
                    .build())

        self.character.inventory.append(item)

        dialog = InventoryDialog(surface_manager = self.surface_manager,
                                 character = self.character,
                                 action_factory = self.action_factory,
                                 parent = None,
                                 flags = Qt.Dialog)

        QTest.mouseClick(find_widget(dialog,
                                     slot_with_item('dagger')),
                         Qt.RightButton)

        assert_that(self.level, does_have_item(item.name))

New function find_widget will iterate through widget hierarchy and return the first widget that satisfied given function (slot_with_item in this case). After the widget has been found, we can use QTest.mouseClick to click it and then assert that desired action has been carried out.

Advantage of this system is that I no longer have to specify exact location of the widget I’m interested clicking or otherwise manipulating. It is enough to write a function that can detect when correct widget has been found and then call find_widget. If I move a widget in another location in widget hierarchy, I don’t necessarily have to update my tests anymore.

Test driven development and user interfaces

I have been using test driven development for my game and have been extremely happy with the results. One section that is lacking with tests is the user interface though. Since I’m currently working on some new controls, I decided to give it a better try.

Qt has good support for testing and PyQt exposes some of the needed classes. Relying only on those however, would probably create rather brittle tests. I rather not hard code names of controls and their hierarchy, if I can avoid it.

This is where Satin comes into play. Currently it is just a readme and license file, but the plan is to write little helpers that can be used to test UI without hardcoding everything:

    dialog = CharacterDialog(character)
    assert_that(dialog, has_label(character.name))

As long as there is a QLabel with text set to character’s name, this assert will pass. It does not matter what the QLabel is named or where it is located.