02 Sep 2015. By William Manley.

Often you will hear us recommend that you should structure your test-packs as a set of library functions, plus test-cases that use those library functions. In this blog post we attempt to make this recommendation more concrete by demonstrating how to apply the “page object” pattern when writing stb-tester test-cases.

Here’s what a test-case can look like when applying the page-object pattern:

def test_that_bbc_one_shows_the_one_show_at_7pm():
    guide = open_guide()
    guide.enter_channel(101)
    guide.scroll_to(time='19:00')
    assert guide.find_selected_programme() == "The One Show"

and this is what it can look like without:

def test_that_bbc_one_shows_the_one_show_at_7pm():
    stbt.press('KEY_GUIDE')
    assert stbt.wait_until(lambda: stbt.match('guide-header.png'))

    stbt.press('KEY_1')
    stbt.press('KEY_0')
    stbt.press('KEY_1')
    time.sleep(3)

    while stbt.ocr(region=stbt.Region(213, 10, 200, 36)) < "19:00":
        stbt.press('KEY_RIGHT')
        time.sleep(1)

    assert stbt.ocr(region=stbt.Region(213, 23, 200, 36)) == "The One Show"

The improvement in understandability is clear. Without the page-object, the meaning of your test-case is buried in the forest of calls to stbt.match, ocr, press and wait_until. Using the page-object pattern means that your test-case consists of high level concepts that expose the intent of the test-case. Notice that with the page-object pattern calls to stbt functions almost never appear in test-cases directly.

Based on Martin Fowler’s diagram for Page Objects in HTML UI testing

In this example the page-object is guide. This is an instance of a class that knows how to understand and navigate your set-top-box’s programme guide.

The class might look like this:

class Guide(object):
    def find_selected_programme(self):
        return stbt.ocr(region=stbt.Region(213, 23, 200, 36))

    def read_lcn(self):
        return stbt.ocr(region=stbt.Region(100, 20, 40, 20))

    def enter_channel(self, lcn):
        for x in "%03d" % lcn:
            stbt.press('KEY_%s' % x)
        assert stbt.wait_until(lambda: int(self.read_lcn()) == lcn)

    def read_programme_time(self):
        return stbt.ocr(region=stbt.Region(213, 10, 200, 36))

    def scroll_to(self, time):
        for _ in range(100):
            orig_time = self.read_programme_time()
            if orig_time == time:
                return
            stbt.press('KEY_RIGHT')
            wait_until(lambda: self.read_programme_time() != orig_time)
        else:
            assert False


def open_guide():
    stbt.press('KEY_GUIDE')
    assert stbt.wait_until(lambda: stbt.match('guide-header.png'))
    return Guide()

The class is used by many test-cases, which increases code reuse. It serves as a central place to handle any known quirks of your UI, and as a single place to update if your UI changes.

If you need to test multiple similar (but slightly different) variants of your UI with a single test-pack, this class is the place to abstract over those differences.

Note that I made open_guide a standalone function rather than a method or static method of class Guide. I don’t consider navigating to the guide to belong to the Guide page-object. Rather it belongs to the app as a whole. The rule of thumb is that the methods on the Guide class are only valid to call if the guide is currently visible.

So, using the page-object pattern:

  • Improves the readability of your test-cases.
  • Reduces the maintenance cost of your test-pack by sharing (and thus reducing) test code and providing a central place to apply fixes and updates.
  • Assists with reusing test-cases for testing multiple variants of a single UI.

In next week’s blog post we will discuss an extension to this approach called the “Frame Object Pattern” which can improve the maintainability of your test scripts even further.

Discuss this on our mailing list