XCTest and the Page Object Model
What's a Page Object?
The page object model is the generally accepted pattern to use when writing automated tests. The gist of it is that in order to create a maintainable test suite, you will reduce duplication in your code by creating an object for each page in your product, and storing all the information and logic around locating elements, performing actions etc. inside the respective page object. Your test code will then interact with the app through these objects.
The page object model has traditionally been applied primarily to websites, but with the introduction of mobile applications, the page object model is now being translated for use with frameworks like Appium and Calabash, which provide an interface with which to test mobile apps. When applied to mobile apps, it is sometimes referred to as the screen object model, due to the difference in terminology between websites and mobile apps.
How do I use Page Objects with XCTest?
XCTest is a framework created by Apple, which doesn't adhere to the WebDriver protocol. This means that the way you use it is a bit different. If you're familiar with Selenium WebDriver, or a relation of it (Appium's interface is an extension of WebDriver's), it can be a bit intimidating to have to learn a new structure.
Here's my translation of how to use page objects with XCTest:
Architecture Overview
My test classes all inherit from XCTestCase. Test classes are named descriptively to quickly communicate the starting state of the tests they contain.
Each test class constructs a reference to a startPage object during setUp(), which is used as the initial state for all tests in that class. setUp() methods may handle the dismissal of any onboarding UI, and will determine that the state of the app is correct e.g. a setUp() method from a descendant of the SignedInTests class will ensure that the user is signed in.
Each test class also holds a reference to a UITestHelper object. This object contains convenience APIs which handle actions involving multiple pages, as it is not appropriate for a page object to contain a method which functions across different pages.
Page objects have been made to represent each page that the tests interact with. These objects encapsulate the implementation details of each page in the app, and are a huge part of conforming to the Don't Repeat Yourself (DRY) principle. Each page object holds a reference to the currently-running test case, where it will be able to access any stored contextual information.
Page objects are used by the tests and the UITestHelper. The page objects expose behavioural APIs to enable the author of the UI tests to write interactions in the same way that a user would think about interacting with what's on their screen.
The TabBarPage object and its descendants implement methods associated with interacting with the UITabBar in the app - which only appears on some pages. While the TabBarPage displays an API of available interactions and knows the logic required for those interactions, it is the TabBarInteractor which houses the implementation details - like a page object for the tab bar, except it's not a page object because its qualities are not unique to a page. The methods available on TabBarPage and its descendants will delegate actions to the TabBarInteractor in order to interact with the UITabBar.
Page Object Structure
Every page object inherits from Page. Page only contains properties and methods that are applicable to every page.
import XCTest class Page: NSObject { // Properties for other pages to inherit let app = XCUIApplication() let testCase: UITestCase class var awaitTimeout: Double class var uniqueElement: XCUIElement class var exists: Bool // Initializer for other pages to inherit init(testCase: UITestCase) // Called on initialization. Waits for the unique element to exist before continuing. // Initialization fails if the unique element is not found within `awaitTimeout` seconds. func await(file: String = #file, line: UInt = #line) }
All page objects will be able to:
Use app to interact with the application and discover the state of elements.
Access testCase to retrieve contextual information about the currently-running test.
Wait for up to awaitTimeout seconds for themselves to load - this will be a higher number for pages which are expected to load more slowly.
Work out whether the page they're representing exists on-screen by searching for the presence of a uniqueElement, which is unique to that page.
The awaitTimeout, uniqueElement and exists properties are variables to allow them to be overridden by descendants, and are class (static) variables because they don't need to be tied to an instance of a page to be calculated. For example, I can find out whether a page exists or not without having to initialize it. If I had to initialize the page, I would have to wait for the awaitTimeout to pass before finding out that the page does not exist, and I would then have to prevent the test from failing
On initialization, the active test case class will be injected and stored, and the page will await itself. This means that the page object will wait for its counterpart in the app to appear on-screen. After a certain amount of time has passed (defined by awaitTimeout), if the page's uniqueElement is still not found, the await method will fail the test using XCTFail().
Awaiting each page on initialization creates a "checkpoint" of sorts - every time the test facilitates a page change, a check is run to ensure that that page really does exist. This enables the test to fail faster if something's not as it expects, and prevents the initialization and attempted use of pages which aren't on-screen.
import XCTest class ProductListPage: TabPage { // Internally accessible properties override class var uniqueElement: XCUIElement override var tabBarScrollableView: XCUIElement? var isEmpty: Bool // Private properties private var productCollection: XCUIElement private var noResultsLabel: XCUIElement private var cells: XCUIElementQuery // Internally accessible methods func goToProductDetailPageForProductAtIndex(index: UInt) -> ProductDetailPage func goToProductDetailPageForProductWithName(name: String) -> ProductDetailPage func goToProductDetailPageForUnsavedItem(offset: Int = 0) -> ProductDetailPage func goToProductDetailPageForSavedItem(offset: Int = 0) -> ProductDetailPage func addItemToSaved(offset: Int = 0) -> String func getIndexForCell(saved: Bool, offset: Int) -> UInt func isSavedForProductWithName(name: String) -> Bool // Private methods private func scrollUpProductList() private func goToProductDetailPage(saved: Bool = false, offset: Int = 0) -> ProductDetailPage private func getCellForProductWithName(name: String) -> XCUIElement private func getCellAtIndex(index: UInt) -> XCUIElement private func getSavedButtonForCell(cell: XCUIElement) -> XCUIElement private func getTitleForCell(cell: XCUIElement) -> String }
The first thing to note here is the application of a page object model principle: Page object methods which change the page (or screen) that the user is viewing will initialize and return a page object representing the new page. This allows test authors to see where, if anywhere, a method will take them.
This rule of having to return a page object from navigational methods is one of the places where using a type-safe language like Swift comes in really handy (instead of a dynamic language like Ruby or Python) - your code won't compile if you're not returning the right type of page. This is not only great for catching programming errors early, but it's also a very powerful way of driving the design of your code; it prevents you from writing overly-configurable methods and forces you to create separation between different paths through the app. Combined with the await part of your page object initialization, this ensures that different paths through the app are represented discretely. This is a cleaner way of designing your classes, and also means that your method names can be more specific about what they do.
Each page descending from Page should override the uniqueElement property to ensure that the class can identify whether or not it exists. For pages with a UITabBar which may disappear from view when the user scrolls through a list, tabBarScrollableView may be overridden to specify a scrollable view which can be interacted with to bring the tab bar back onto the screen.
Not all properties should be public. In general, all properties and methods returning XCUIElement or XCUIElementQuery objects should be private, to prevent any other objects from interacting with them. Page objects should only interact with their corresponding page, and no other objects should be able to interact directly with the app.
The methods which are internally accessible (no need for these to be public, as these classes won't be used by a third party) all represent either actions that the user could perform on this page, or information from the page that the user would be able to see. Information may take the form of a get... method or a Boolean calculation.
The private methods are those which are only useful to this page. No other objects should be interested in using them - they are assistants to the internally accessible methods, but they have no place being used by other objects.
Test Structure
The tests themselves are structured in pretty much the same way as any test that uses page objects.
import XCTest class SignedInTests: UITestCase { /** Performs test setup for all signed in tests. - Signs in if the user is signed out on launch. - Returns to the home page for the test to begin. */ override func setUp() { super.setUp() // Sign in if !isSignedIn { let meUnauthenticatedPage = startPage.goToMeTabExpectingRootUnauthenticatedPage() let signInPage = meUnauthenticatedPage.goToSignInPage() let meAuthenticatedPage = signInPage.signInAsValidUserExpectingMePage(user!) startPage = meAuthenticatedPage.goToHomeTabExpectingRootPage() } } // Performs test tear down. override func tearDown() { super.tearDown() } /** Given I am signed in When I sign out Then I will be signed out */ func testSignOut() { // Sign out let meAuthenticatedPage = startPage.goToMeTabExpectingRootAuthenticatedPage() meAuthenticatedPage.signOut() // Check you are signed out XCTAssertTrue(MeUnauthenticatedPage.exists) } }
The test class is called SignedInTests so the setUp() function ensures that you are signed in before the test begins, making sure that startPage is set to the home page again before it finishes. It also calls super.setUp() before anything else - ensuring that all of the setup actions it inherited are done too.
The test begins by navigating away from startPage. It brings up the MeAuthenticatedPage (in the app I'm testing, the "Me" page (which shows information about me) is completely different depending on whether you're signed in or not, so they are separate page objects.) and signs out. The signOut() method returns a MeUnauthenticatedPage object, but as I'm not going to do anything with that page, there's no point in storing it. The last part of the test is the assertion - in this case, checking that the page I expect to be on-screen does exist.
Under the hood, there's a lot of lines of code going into making this test work, but thanks to my architecture, most of those lines are obscured. The test code becomes very readable and easy to follow, and the test suite as a whole is easy to maintain, as the implementation logic is encapsulated in page objects.








