Overview:
If you are an experienced automation engineer, you would know better that maintaining an automated test framework/project is NOT an easy task! You could easily create a page object and a test which works just fine / get things done. But what happens once you have thousands of tests? How long does it take to look into the failures as part of your continuous regression? If you touch something in the code, does something else break? Do you have to have a dedicated person to look into the test suite failures? Then the problem could be with your test framework!
Proper design techniques/principles /best practices should be followed right from beginning – not only for the Page Objects design, but also for your tests and test data.
We already have seen many page objects related designs in our TestAutomationGuru articles. You can check here for the list of articles. Lets see how we should model / design our testNG test in this article.
Sample Workflow:
In order to explain things better, I am considering below workflow which I have used in the previous article.
I am assuming that we have created page objects for the above pages as I had mentioned here. I also assume that all the page objects extend the below abstract page.
public abstract class Page {
public abstract boolean isAt();
}
The idea here is all the page objects should have a ‘isAt‘ method which returns a boolean to confirm if they are in the appropriate page as shown below. [Inspired from Jon Sonmez‘s idea]
@Override
public boolean isAt(){
return this.username.isDisplayed();
}
They all also have a static entry point ‘init‘ to create an instance and chain other methods of the page object to improve the readability.
private UserDetailsPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public static UserDetailsPage init(WebDriver driver) {
return new UserDetailsPage(driver);
}
Test Steps:
I need to have below simple steps to ensure that user registration functionality of the application works fine.
- launch
- enter user details
- search for a product with specific criteria & select
- enter payment info & submit
- verify the order confirmation to complete the registration process
As part of the registration flow, I need to cover if the application accepts different input parameters.
For ex:
- Ordering product with just CC.
- Ordering product with CC + Promocode etc
In order to design our tests better, We need to ensure that we follow some of the best practices.
Each Test Step/Checkpoint should be a @Test method:
Do not place all the driver invocation, page object creation, test data in a single test method. Instead try to create separate test methods for each step you would like to perform / validate.
For ex: For the above test steps, Lets create a test template as shown here. We need to call the methods in a sequence. So I use dependsOnMethods.
public class UserRegistrationTest {
@BeforeTest
public void setUpDriver(){
// setup driver
}
@Test
public void launch() {
// launch site
}
@Test(dependsOnMethods = "launch")
public void enterUserInfoAndSubmit() {
// enter user details
}
@Test(dependsOnMethods = "enterUserInfoAndSubmit")
public void searchProductAndSubmit() {
// search for a product with specific criteria & select
}
@Test(dependsOnMethods = "searchProductAndSubmit")
public void enterPaymentInfoAndSubmit() {
// enter payment info & submit
}
@Test(dependsOnMethods = "enterPaymentInfoAndSubmit")
public void validateOrderConfirmation() {
// verify the order confirmation
}
@AfterTest
public void tearDownDriver(){
// quit driver
}
}
Instance Variables:
We might need to reuse some of the objects like page objects, test data, driver instances in other methods in the Test class. So lets create them as instance variable.
private WebDriver driver;
private UserDetailsPage userDetailsPage;
private ProductSearchPage productSearchPage;
private OrderSummaryPage orderSummaryPage;
private OrderConfirmationPage orderConfirmationPage;
Driver Manager:
Do not handle all the driver management in the test class / base test class. Always follow Single Responsibility Principle. Create a separate DriverManager factory class to get you the specific WebDriver instance you want as shown here.
@BeforeTest
public void setUpDriver(){
driver = DriverManager.getChromeDriver();
}
If we would like to get the browser through testNG suite file as shown here,
<test name="user registration test - USA user">
<parameter name="browser" value="chrome" />
<classes>
<class name="com.testautomationguru.UserRegistrationTest" />
</classes>
</test>
then, we could modify the setup method as shown here.
@BeforeTest
@Parameters({ "browser" })
public void setUpDriver(String browser){
driver = DriverManager.getDriver(browser);
}
Assertion:
Our workflow starts with UserDetailsPage. Using ‘init‘ method, I create the new instance method. Always validate if the expected page has loaded before interacting with the page. If the page itself is not loaded, then there is no point in proceeding with the test. So when the assertion fails, all the dependent methods are skipped.
@Test
public void launch() {
userDetailsPage = UserDetailsPage.init(driver)
.launch();
//validate if the page is loaded
Assert.assertTrue(userDetailsPage.isAt());
}
We could overload the isAt method with some timeout if the test should have some control over it.
Assert.assertTrue(userDetailsPage.isAt(10, TimeUnit.SECONDS));
We do not need to add this for each and every page. Instead modifying abstract Page with the below method will take care! Try to use ‘Awaitility‘ to handle page synchronization. It is much better and easier than WebDriverWait.
public abstract class Page {
public abstract boolean isAt();
public boolean isAt(long timeout, TimeUnit timeunt){
try{
await().atMost(timeout, timeunit)
.ignoreExceptions()
.until(() -> isAt());
return true;
}catch(Exception e){
return false;
}
}
}
Never hard-code Data:
Do not hard-code the test data in your test. You might want to reuse this test for multiple input parameters.
@Test(dependsOnMethods = "launch")
public void enterUserInfoAndSubmit() {
userDetailsPage.setFirstname(".....") // test-data to be passed
.setLastname(".....") // test-data to be passed
.setDOB(".....") // test-data to be passed
.setEMail(".....") // test-data to be passed
.setAddress(".....") // test-data to be passed
.submit();
//validate if the page is loaded
Assert.assertTrue(ProductSearchPage.init(driver).isAt());
}
Never Pass ‘too many parameters’:
Hard-coding is not good. Passing all the values as String from the suite file might sound OK.
<test name="user registration test - USA user">
<parameter name="browser" value="chrome" />
<parameter name="firstname" value="fn" />
<parameter name="lasttname" value="ln" />
<parameter name="dob" value="01/01/2000" />
<parameter name="email" value="noreply@gmail.com" />
<parameter name="address" value="123 non main st" />
<parameter name="CC" value="1234-1234-1234-1234" />
<parameter name="billingaAddress" value="123 non main st" />
<classes>
<class name="com.testautomationguru.UserRegistrationTest" />
</classes>
</test>
But it makes the test & suite file very difficult to read / manage – with too many parameters!
@Test(dependsOnMethods = "launch")
@Parameters("firstname", "lastname", "dob"........)
public void enterUserInfoAndSubmit(String firstnam, String lastname, String dob......) {
userDetailsPage.setFirstname(firstname)
.setLastname(lastname)
.setDOB(dob)
.setEMail(".....")
.setAddress(".....")
.submit();
//validate if the page is loaded
Assert.assertTrue(ProductSearchPage.init(driver).isAt());
}
Test Data Management:
Instead of keeping everything in a suite file, Lets keep our test data in a separate json file as shown here or any other format you prefer.
{
"firstname":"testautomation",
"lastname":"guru",
"dob":"01/01/2000",
"email":"noreply@gmail.com",
"address":"123 non main st",
"cc":{
"number":"4444-4444-4444-4444",
"billing":"123 non main st"
},
"product":{
"searchCriteria":"criteria"
}
}
To de-serialize above json file into a Java object, Lets model a class by using jaskson library. [I have intentionally ignored getters in the below class to reduce the length of the article]
public class Data {
@JsonProperty("firstname")
String firstname;
@JsonProperty("lastname")
String lastname;
@JsonProperty("dob")
String DOB;
@JsonProperty("email")
String email;
@JsonProperty("address")
String address;
@JsonProperty("cc")
CC cc;
@JsonProperty("product")
Product product;
static class CC {
@JsonProperty("number")
String number;
@JsonProperty("billing")
String billingAddress;
}
static class Product {
@JsonProperty("searchCriteria")
String criteria;
}
public static Data get(String filename) throws JsonParseException, JsonMappingException, IOException{
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(new File(filename), Data.class);
}
}
Then, lets go back to our setup method to modify slightly to create the data instance.
private WebDriver driver;
private Data data;
private UserDetailsPage userDetailsPage;
private ProductSearchPage productSearchPage;
private OrderSummaryPage orderSummaryPage;
private OrderConfirmationPage orderConfirmationPage;
@BeforeTest
@Parameters({"browser", "user-registration-data-file"})
public void setUpDriver(String browser, String userDataFile){
driver = DriverManager.getDriver(browser);
data = Data.get(userDataFile);
}
Now we could pass the test multiple data file through suite as shown here. This looks cleaner than the previous approach with tons of String parameters.
<suite name="TestAutomationGuruSuite">
<test name="user registration test - USA user">
<parameter name="browser" value="chrome" />
<parameter name="user-registration-data-file" value="/src/test/resources/usa-user.json" />
<classes>
<class name="com.testautomationguru.UserRegistrationTest" />
</classes>
</test>
<test name="user registration test - India user">
<parameter name="browser" value="chrome" />
<parameter name="user-registration-data-file" value="/src/test/resources/india-user.json" />
<classes>
<class name="com.testautomationguru.UserRegistrationTest" />
</classes>
</test>
</suite>
Now, our ‘data‘ object has all the data we need to proceed with our test. Just use the getters wherever you need the test data.
@Test(dependsOnMethods = "launch")
public void enterUserInfoAndSubmit() {
userDetailsPage.setFirstname(data.getFirstname())
.setLastname(data.getLastname())
.setDOB(data.getDOB())
.setEMail(data.getEMailAddress())
.setAddress(data.getAddress())
.submit();
//validate if the page is loaded
Assert.assertTrue(ProductSearchPage.init(driver).isAt());
}
Summary:
Following design principles / best practices initially could be little bit annoying or time consuming. This is because, We all want to quickly get the job done. We do not spend a lot of time in reviewing the tests/test data design. We do not worry about 1000 more tests we might be automating in future. To have successful automated test framework, invest sometime in following all the design principles, best practices etc even for the test scripts design/development as well.
Happy testing & Subscribe 🙂
AWESOME! =) thanks
Glad that you find it useful.
Could this work with data provider and an array of elments or objects?
Yes. JSON supports array structure.
I don’t quite understand how you are able to do:
userDetailsPage.setFoo(…)
.setBar(…)
Are these setters defined in your page object class?
Yes. thats the assumption.
Do you have this example in GitHub or somewhere?
No. I thought it was not worth adding as a project in Github as it is kind of a best practice.
Awesome, its very useful. Thanks
Great post! I appreciate all the work you put into this site. This is really a nice and informative article. Article containing all useful information about How To Design Tests and Test Data with selenium web-driver. Each step were explained in detailed way. Very handy. Keep writing, you’re doing great!
Awsome stuff . can i get source code for the same ???
Awesome….thanks a lot
Hi Vinoth,
I am having a doubt that how can we pass the URL here.
Great article, thanks.