Applying the "page object" pattern with stb-tester
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.