Overview:
As a software engineer, We all face some errors/exceptions while writing code! So what do we do when we face such a problem? If we are not sure, We google for solutions immediately. Don’t we? We google because we know that we would not be alone and someone would have already found the solution, for the problem we are facing now, which we could use to solve our problem.
Well, What to do if we face an issue in the high level software design – when connecting different classes / modules – when you have only a vague idea!! How can we google such things when we ourselves are not sure of the problem we have in the first place!!
No worries, You still have a solution!
Design patterns are the solutions for the problems which you might face in the software design!!
Design Patterns are well optimized and reusable solutions to a given problem in the software design. It helps us to show the relationship among the classes and the way in which they interact. Design pattern is a template which you have to carefully analyze and use it in appropriate places.
More information on design pattern can be found here.
Design Patterns in Test Automation:
As an automated test engineer, should we really care about design principles and patterns? Why do we need to learn/use Design Patterns in functional test automation?
After all, Test automation framework/automated test case is also a software which is used to test another software. So, we could apply the same design principles/patterns in our test automation design as well to come up with more elegant solution for any given design related problem in the test automation framework.
Remember that Design Patterns are NOT really mandatory. So you do not have to use them! But when you learn them, you would know exactly when to use! Basically these are all well tested solutions. So when you come across a problem while designing your framework/automated test cases, these design patterns could immediately help you by proving you a formula / template & saves us a lot of time and effort.
Note: Your aim should not be to implement a certain pattern in your framework. Instead, identify a problem in the framework and then recognize the pattern which could be used to solve the problem. Do not use any design pattern where it is not really required!!
In this article, We are going to see where we could use the Chain Of Responsibility Pattern which is one of the Behavioral design patterns.
Problem Statement:
I had an application to automate in which we had many business logic to show the next screen. We kept introducing new screens, modifying existing ones or removing screens. It was very challenging to automate the application. I needed to find a solution to minimize the overall maintenance effort. Chain Of Responsibility helped me to achieve what we wanted!
Lets consider a sample application in which the business workflow is as shown here.
- In order to purchase a product from the site, the user should have been registered.
- The site offers some free/trial products. When the price is $0, the payment info page is simply skipped, the site just shows the order confirmation page.
- Existing user can view his existing order / create new order after logging into the site.
Above workflow might look simple. But there are few conditions based on that we display appropriate pages. If the business wants to introduce new screens/conditions in the workflow, then it will add more complexity in the test design. To test the application behavior, we need to consider all the possible combinations of the workflow, prepare tests & test data accordingly.
Lets see how we could handle this complexity using Chain Of Responsibility pattern.
Chain Of Responsibility Pattern:
Chain of Responsibility pattern helps to avoid coupling the sender of the request to the receiver by giving more than object to handle the request. More info is here. To understand this better, Lets consider the ATM machine example.
- If I need to withdraw $180, I could expect 1 $100, 1 $50, 1 $20 and 2 $5 from the ATM machine. If $100 is not available, Then I could expect 2 $50 instead.
Handling this logic ourselves in the code is very difficult to maintain.
Traditional Approach:
if(amount > 100){
System.out.println("Dispatching $" + 100 + " x " + amount / 100);
amount = amount % 100;
}
if(amount > 50){
System.out.println("Dispatching $" + 50 + " x " + amount / 50);
amount = amount % 100;
}
if(amount > 20){
System.out.println("Dispatching $" + 20 + " x " + amount / 20);
amount = amount % 20;
}
...
Instead of the above approach, we create all the possible handlers like 100_handler, 50_handler etc… then we chain these handlers to together as one single handler.
Lets see how we could create this ATM example in Java using Java8 Functional Interfaces.
static Function<Integer, Integer> getHandler(int handler) {
return (amount) -> {
if (amount >= handler) {
System.out.println("Dispatching $" + handler + " x " + amount / handler);
}
return amount % handler;
};
}
Function<Integer, Integer> handler100 = getHandler(100);
Function<Integer, Integer> handler50 = getHandler(50);
Function<Integer, Integer> handler20 = getHandler(20);
Function<Integer, Integer> handler10 = getHandler(10);
Function<Integer, Integer> handler5 = getHandler(5);
Function<Integer, Integer> handler1 = getHandler(1);
//now we chain these handlers together as 1 single handler
Function<Integer, Integer> ATM = handler100.andThen(handler50)
.andThen(handler20)
.andThen(handler10)
.andThen(handler5)
.andThen(handler1);
ATM.apply(183);
Running the above code displays the following as expected.
Dispatching $100 x 1
Dispatching $50 x 1
Dispatching $20 x 1
Dispatching $10 x 1
Dispatching $1 x 3
Now If the government tries to introduce new denomination, say $500 and $30, it is easy to include them in the chain. The caller does not need to worry about the new denomination.
Lets see how we could apply similar pattern for our workflow.
Page Object Design:
We already have enough articles on Page Objects Design in TestAutomationGuru. I have followed Advanced Page Object design to create the page. That is, all the components of the page are considered as separate fragments. If you have not read about that page object design before, I would suggest you to read that first.
Lets assume that we have page objects for the above pages in the workflow. Aim of this article is to design the test using Chain Of Responsibility pattern.
Test Data Design:
Lets assume we have test data as shown here. Each and every test case will have their own json file. Check this article here for more on test data design.
{
"existingUser": false,
"user":{
"firstName":"automation",
"lastname":"guru",
"username":"guru001",
"password":"password1"
},
"product":{
"searchCriteria":"book",
"price":"14.00"
},
"paymentInfo":{
"method":"CC",
"cardNumber":"4111111111111111",
"expiry":"0430",
"cvv":"123"
}
}
Test Design:
First I create a class which holds all the pages. I assume that there is a TestData class which retrieves the above data from the json file. If it is not clear, please check this article on the Test Data design.
public class ApplicationPages {
private LoginPage loginPage;
private RegistrationPage registrationPage;
private HomePage homePage;
private ProductSearchPage productSearchPage;
private PaymentInfoPage paymentInfoPage;
private ConfirmationPage confirmationPage;
private ProductViewPage productViewPage;
private LogoutPage logoutPage;
public ApplicationPages(WebDriver driver){
this.loginPage = new LoginPage(driver);
this.registrationPage = new RegistrationPage(driver);
this.homePage = new HomePage(driver);
this.productSearchPage = new ProductSearchPage(driver);
this.paymentInfoPage = new PaymentInfoPage(driver);
this.confirmationPage = new ConfirmationPage(driver);
this.productViewPage = new ProductViewPage(driver);
this.logoutPage = new LogoutPage(driver);
}
public final UnaryOperator<TestData> userLoginPage = (d) -> {
if(d.isExistingUser()) {
assertTrue(loginPage.isAt());
loginPage.setUsername(d.getUsername());
loginPage.setPassword(d.getPassword());
loginPage.signin();
}else{
loginPage.goToRegistrationPage();
}
return d;
};
public final UnaryOperator<TestData> userRegistrationPage = (d) -> {
if(!d.isExistingUser()){
assertTrue(registrationPage.isAt());
registrationPage.setUsername(d.getUsername());
registrationPage.setPassword(d.getPassword());
registrationPage.register();
}
return d;
};
public final UnaryOperator<TestData> userHomePage = (d) -> {
assertTrue(homePage.isAt());
if(d.getProductSearchCriteria() != null){
homePage.goToProductSearchPage();
}else if(d.getOrderNumber() != null){
homePage.goToProductView(d.getOrderNumber());
}
return d;
};
public final UnaryOperator<TestData> userProductSearchPage = (d) -> {
if(d.getProductSearchCriteria() != null){
assertTrue(productSearchPage.isAt());
productSearchPage.search(d.getProductSearchCriteria());
productSearchPage.selectProduct();
d.isProductSelected(true);
}
return d;
};
public final UnaryOperator<TestData> orderPaymentInfoPage = (d) -> {
if(d.getProductPrice() > 0){
assertTrue(paymentInfoPage.isAt());
paymentInfoPage.enterCC();
paymentInfoPage.submit();
}
return d;
};
public final UnaryOperator<TestData> orderConfirmationPage = (d) -> {
if(d.isProductSelected()){
assertTrue(confirmationPage.isAt());
d.setOrderNumber(confirmationPage.getOrderNumber());
confirmationPage.clickOnViewProduct();
}
return d;
};
public final UnaryOperator<TestData> userProductViewPage = (d) -> {
if(d.getOrderNumber() != null){
assertTrue(productViewPage.isAt());
assertEquals(d.getOrderNumber(), productViewPage.getOrderNumber());
productViewPage.logout();
}
return d;
};
public final UnaryOperator<TestData> userLogoutPage = (d) -> {
assertTrue(logoutPage.isAt());
return d;
};
}
Then we create the test class which chains all the pages as shown here and pass the test data to the first handler. Rest will be taken care by the handlers/individual pages depends on the application workflow.
public class ApplicationWorkflowTest {
private ApplicationPages app;
@Test
public void workFlowTest(){
TestData testData = TestData.get("TC001");
this.app = new ApplicationPages(DriverManager.getDriver("chrome"));
this.getAppWorkflow().apply(testData);
}
private Function<TestData, TestData> getAppWorkflow() {
return app.userLoginPage.andThen(app.userRegistrationPage)
.andThen(app.userHomePage)
.andThen(app.userProductSearchPage)
.andThen(app.orderPaymentInfoPage)
.andThen(app.orderConfirmationPage)
.andThen(app.userProductViewPage)
.andThen(app.userLogoutPage);
}
}
We do not need to create multiple test classes each possible work flow. Above test class is enough for new user registration, existing user login, free product flow etc.
Summary:
Chain Of Responsibility is extremely useful when there are multiple dynamic pages in the business work flow. The test does not really handle the business workflow complexity in the above example! The test is really simple with single test method. The workflow chain takes care of that. Whenever a new page is introduced, we need to include the page in the chain. No need to create any separate test class. Instead create a test json file to provide test data to cover the page.
Happy Testing & Subscribe 🙂