By William Manley. 02 Sep 2015
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:
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.