TLDR; use abstractions for precision and create new abstraction layers which model state and data flow rather than adding it to lower level classes.
This post was created as a response to a question I was asked over email regarding approaches for modelling data re-use in abstractions.
Question
I’ll paraphrase the question I was asked as:
“When we have data used in a test that is used on many page objects, should I pass the data between page objects, hard code the data or leave it in the test for assertions?”
Concepts
The underlying concept here, I think, is “Which abstractions do I create for automating?”
There are many different approaches to this, and some people will provide absolute answers of “Use pattern X”.
I’m going to put the responsibility on you to figure out the most appropriate way for you to model the execution of your application. Because that’s what we do when we automate. We model the execution of the application. We model it in whatever form the tooling we use to automate takes e.g. high level DSL, diagram, code.
A useful external concept to hold on to is “The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise” from Dijkstra The Humble Programmer
I take this to mean, model the application as appropriately as you need to.
Then I keep a few coding guidelines in mind:
- avoid tightly coupling classes
- pass in only the data that the classes will use
- add new classes as required, don’t just add code in the first class that seems helpful
- if you do just add code in the first class that seems helpful then remember to refactor once it is running successfully
And the answer I provided to the question was…
It is possible to pass information across pages, since each page is a class we add methods or params that we want.
But, we probably don’t want to manage this in the page and have it passed between pages because then the pages become more dependent upon each other and more tightly coupled and they are maintaining objects that are not strictly relevant to the page object.
The page object - helps us interact with the page - that’s the real limit to its responsibilities.
I don’t like to add extras to a page object that go beyond this.
The ’test’ however, knows exactly what process flow we want to do, and what assertions to make, so it uses the page objects and any other necessary objects to implement the test. The test maintains state.
Initially test code is likely to have hard coded data e.g. this is pseudo code, I don’t expect it to work
@Test
public void searchForATerm(){
SearchPage sp=new SearchPage();
sp.open();
sp.submitSearchForm("Location 1")
SearchResultsPage srp=new SearchResultsPage();
Assert.assertEquals( srp.getTitle(), "Location 1 Results");
// do something else for "Location 1"
}
If I wanted to perform multiple searches for this test then I might make the test a data driven test so that “Location 1” is passed to the method by some sort of data provider using whatever mechanism the Test Execution Framework provides.
If I later wanted to perform that process regularly then I might create a ‘process’ object e.g.
public class SearchProcess{
public SearchProcess(WebDriver driver, String searchTerm){
// store params as fields
}
public void search(){
SearchPage sp=new SearchPage();
sp.open();
sp.submitSearchForm(this.searchTerm)
SearchResultsPage srp=new SearchResultsPage();
new WebDriverWait(driver,10).until(pageTitleMatches(this.searchTerm +" Results"));
// do other stuff related to the process and searchTerm
}
}
NOTE: I might not have the
WebDriverWait
in there, I left that to show the ’re-use’ of the data and to maintain the semantics of the@Test
code that I refactored in. Modelling Automating is a series of decisions, and these are based on the context you work within: who maintains the code? how often does the application change? how likely is it that this assertion will vary? how fast does this code need to run? etc.
Then I’d change the test to use the SearchProcess instead of the page object calls e.g.
@Test
public void searchForATerm(){
new SearchProcess(driver, "Location 1").search();
// do something else for "Location 1"
}
I don’t like to have pages pass information to each other, but I’m happy for a conceptual “process” object to maintain the information required for that process, and for it to utilise any Page Objects, API calls, Data Base calls etc that it needs to implement the process.
Similarly for an @Test - the Test should probably maintain all the information it needs to implement the test, rather than other objects.
Avoiding coupling between objects and keeping their scope of responsibility tight can help long-term maintenance.
I am usually happy to add more classes to the code which represent the abstractions that I’m dealing with when modelling the execution.
Summary
- only pass to classes and methods the information they need to do their job
- avoid tightly coupling classes
- create new classes which use Page Objects, APIs, etc. which maintain the state/data across all those calls
- abstract for precision of - understanding, maintenance, re-use, etc.
Links to “The Humble Programmer” by Edsger W. Dijkstra: