We can split the definition of Process, Page and Control Objects into assemblies with interfaces and implementations:
- Interfaces define which controls and operations a page provides, e.g. page
LoginWindow
may have buttonLogin
and methodLoginUser(username, password)
. - Implementations, in contrast, define how these controls are located (e.g. in the DOM tree) and how operations are broken down into actions sequences on the page controls or other operations, e.g. Button
Login
is located by searching for a control with taga
(a link) and text "Sign in", and a user is logged in via methodLoginUser
by entering the user name and password into input fieldsUsername
andPassword
, and finally clicking buttonLogin
.
If both layers are separated, the test cases can be defined in another assembly so that they solely reference the interface layer, while their implementations are loaded and resolved dynamically, i.e. when the test executes.
Tests cases and page objects implementations are thus decoupled.
Decoupling tests case and page objects has several benefits.
Better test maintanance and reusability: If the interface remains unchanged, the same test fits for different user interface versions (no code or reference change). As an example, if a test invokes interface method LoginUser(user, password)
on page object interface ILoginWindow
, it does not care about the concrete actions that need to be taken to log on the given user: Adding a check box "Accept the terms and conditions", e.g., needs to be adapted in the implementation, yet the page interface remains fully unchanged, and so does the test code and its references.
Tests cases can be coded when the product is still under development: Tests can be coded directly after, e.g., the UX has finished the design and the user stories (and associated interfaces), and hence before the pages' functionality has been implemented.
Systems consisting of many components or developed concurrently by many teams can be tested easier: When several components with user interfaces are developed concurrently and then integrated, and we want to code test cases for that integration: User interface tests for the integrated system can be coded once every component provides its set of page interfaces. A test-driven approach can be realized up to the system-level.
The tests and page objects can be decoupled via interfaces as follows:
- Create an interface assembly
- Create a new assembly for page object interfaces (the "interface assembly").
- Add the interfaces for the page objects that shall be exposed, e.g.,
IMyTab
,ILink
,IMenu
for classesMyTab
,Link
, andMenu
, respectively.
- Create an implementation assembly
- Add the interface assembly to the list of referenced projects in the page object assembly.
- Move all tab, control and page objects into that assembly.
- Extend the page objects by the respective interface, e.g.,
class Menu : PageObject, IChildOf<MyTab>
turns intoclass Menu : PageObject, IChildOf<MyTab>, IMenu
etc. - Replace all types these classes return by their corresponding interface, e.g., the property
Link Events => Find<Link>...
in page objectMenu
turns intoILink Events => Find<ILink>
. TheILink
will be resolved at runtime.
- Adapt the test assembly
- Ensure that all tests are defined in an assembly other than the interface or implementation assembly.
- Make sure this assembly has a reference to the interface assembly, not however a direct or indirect reference to the page object assembly.
- Replace all usages of page and control object implementations by their corresponding interface, i.e.
tab.On<Menu>()
turns intotab.On<IMenu>()
etc. - Locate the tab object via the
TabObject.Resolve
method, e.g.var tab = new MyTab()
turns intovar tab = TabObject.Resolve<IMyTab>()
. - Finally, ensure the page object assembly is available and loaded, e.g., via
Assembly.LoadFrom
, before tests get executed.