Friday, August 18, 2023

How to build a test automation framework using Playwright, Spring boot, Java, Cucumber and Junit5

In this article, I will be guiding you step by step in creating a test automation framework using Playwright, Spring boot, Java, Cucumber and Junit5. Additionally, I will explain about Aspect Oriented Programming (AOP) for cross-cutting features (like logging) and using Lombok annotations for simplification and enhancing readability of the code.

What do you need before starting?

I always recommend to use latest tech to explore full potentials of various software's. Its absolutely fine if someone disagrees with this approach 😃

  • A 64 bit system with at least 4GB RAM (8 GB preferred).
  • MacOS - Monterey or Windows 10 and above
  • Java 11 or above installed
  • IntelliJ Community edition (latest version preferred) installed
  • Docker Desktop installed
How to create a Spring boot project?

The simplest way, is to visit this site: https://start.spring.io/, then select from the available options, enter value for Group, remember this will become your parent package. Based on your requirement, you may decide to enter Artifact, Name, Description.

Here, I have selected Project as "Maven". If you want to
 experiment with "Gradle-Groovy" you are most welcome, do leave a comment  and let me know if you faced any issue, it will help me gain some knowledge.

For Language, I have selected Java, as our objective is to write code using java. It is recommended to use latest Spring Boot version, as it will always have new features in it.

Packaging type "Jar" is selected by default, and I don't have any objective to change it, so left it as is. Java version also I left as is.

You might have noticed, there is an option to add dependencies:

This is a useful option, through which we can add a lot of needed dependencies for spring boot , which will be automatically added to the maven pom file, once we click on the "Generate" link.

I will be adding Lombok dependency as follows:

Click the "Add Dependencies" link, then select Lombok and press enter.








Now, you can see that Lombok has been selected as a dependency:






Now you are ready to create the project. Click "Generate" button. It will automatically download the project skeleton as a zip file. The file name will be as per the "Name" parameter which you have passed.

 











This is how the project folders will look like. Until unless, you are planning to create some Micro-services for your hands on with Playwright API testing, you may remove the "main" folder from the project, before importing it as a maven project in IntelliJ.

What maven dependencies do we need to add?
  • Cucumber-java
  • Cucumber-spring
  • Cucumber-junit-platform-engine
  • Playwright
  • junit-platform-suite
  • Commons-lang3
  • aspectjweaver
You can refer to the Maven POM file: POM

Now, I will explain how to create Playwright browser using factory design pattern. The code is available in my GitHub repository:   PlaywrightTestAutomationFramework

Step 1:

Create a package (folder), called configuration and park your Cucumber and Spring configuration classes in it.

import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.test.context.ContextConfiguration;

@CucumberContextConfiguration
@ContextConfiguration(classes={TestConfig.class})
public class CucumberContextConfig {

}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("springboot.playwright.cucumber")
public class TestConfig {

}
Step 2:

Create a package (folder) to park all interfaces and classes that are responsible for creating and supplying Playwright bean. Next, create a Java Interface, which will supply the playwright browser factory bean:


Here, I have created some method definitions. 

setPlaywrightBrowser(String browserType) method will take the browser type as input, say Chrome/Firefox etc. and will initialize the Playwright browser context (native to Playwright).

getPlaywrightBrowser() method will return the playwright bean browser context.

setPlaywrightBrowserContext(), getPlaywrightBrowserContext(), setPlaywrightPage() and getPlaywrightPage() methods are created for support purposes, in case the user need to access these from the factory bean. setTracing(Boolean) and isTracingOptionSet() are also support methods, to turn the browser tracing on/off.

Now, create a class that will implement the interface:

The first line within the class is very important:  Playwright playwright = Playwright.create();

The rest of the code is self-explanatory, carefully read and understand how it Overrides the PlaywrightBrowser interface, created in Step 1 above.

import com.microsoft.playwright.*;
import java.util.Objects;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

@SuppressWarnings({"unused","cast"})
public class PlaywrightBrowserSupplier implements PlaywrightBrowser {
Playwright playwright = Playwright.create();
Browser browser;
BrowserContext browserContext;
Page page;
//This boolean value is used for introducing Playwright tracing
Boolean isTracingSet=false;

public PlaywrightBrowserSupplier(String BrowserType, Optional<Boolean> tracingOption){
setPlaywrightBrowser(BrowserType);
//This method checks the optional tracingOption value and accordingly turns on
// Playwright browser tracing
setTracing(tracingOption.orElse(false));
}

/** This method will initialize the PlaywrightBrowser bean (Facade design pattern)
* with appropriate browser type
* Author: Susnigdha Chatterjee
*/
@Override
public void setPlaywrightBrowser(String browserType)
{
switch(browserType.toLowerCase()) {
case "chrome":
//Below line will launch chrome browser in non-headless mode
//browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
// .setHeadless(false));
//If you wish to execute the code in Headless mode then please un comment
// below line*/
browser = playwright.chromium().launch();
break;
case "firefox":
browser = playwright.firefox().launch();
break;
}
setPlaywrightBrowserContext();
setPlaywrightPage();
}

@Override
public Browser getPlaywrightBrowser() {
return browser;
}

@Override
public void setPlaywrightBrowserContext() {
browserContext=getPlaywrightBrowser()
.newContext();
}

@Override
public BrowserContext getPlaywrightBrowserContext() {
return browserContext;}

@Override
public void setPlaywrightPage(){
page=getPlaywrightBrowserContext().waitForPage(()->getPlaywrightBrowserContext()
.newPage());
}

/**This method will help to retrieve the Playwright page from the PlaywrightBrowser bean
*/
@Override
public Page getPlaywrightPage(){
return page;
}

@Override
public void close() {
page.close();
browserContext.close();
playwright.close();
}

/**
* This method will be called from Hooks class, from cucumber After hook
* @author Susnigdha Chatterjee
* @return byte array which holds the screenshot
*/
@Override
public byte[] captureScreenshot(){
Path objPath = Paths.get("target/screenshots/Screenshot_"+ LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("ddMMyy_hhmmss"))+".png");
return page.screenshot(new Page.ScreenshotOptions().setPath(objPath)
.setFullPage(true));
}

/**
* This method will be called from Constructor to set the Playwright browser tracing on
* @author Susnigdha Chatterjee
* @return byte array which holds the screenshot
*/
@Override
public void setTracing(Boolean option){
if(option && !Objects.isNull(browserContext)){
browserContext.tracing().start(new Tracing.StartOptions()
.setSnapshots(true));
isTracingSet = true;
}
}
/**
* This method will be called from Hooks class's After annotation to decide if
* Playwright browser tracing needs to be turned off
* @author Susnigdha Chatterjee
* @return byte array which holds the screenshot
*/
@Override
public boolean isTracingOptionSet(){
return isTracingSet;
}
}

Finally. create a "Configuration" class, that will be responsible for creating the factory bean. Pay attention to the annotations used. @Configuration will inform Spring that it has Bean definitions. @Slf4j annotation comes from Lombok, which will use Spring's default Log Back mechanism to generate console logs. @SupressWarnings will suppress the listed unwanted warnings. @Bean annotation indicates that the method will create a bean and @ScenarioScope will indicate that this bean will be available across Cucumber scenarios. Rest of the code is self-explanatory, you need to read :) . Do leave a comment for any doubts.
import io.cucumber.spring.ScenarioScope;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Optional;

/**
* Using factory design pattern. The PlaywrightBrowser interface serves as the factory.
* PlaywrightBrowserSupplier implements the PlaywrightBrowser interface based on
* the browser type supplied using the browserType parameter of
* testconfigurations.properties file
*/

@Configuration
@Slf4j
@SuppressWarnings({"unused","cast"})
public class PlaywrightInitializer {
/** Using Facade design pattern to initialize Playwright page using
* custom built PlaywrightBrowser interface
* @author Susnigdha Chatterjee
*/
@Bean(name="PlaywrightBrowser", destroyMethod = "close")
@ScenarioScope
public PlaywrightBrowser init() {
//Creating the bean of type PlaywrightBrowser using its implementing
// class PlaywrightBrowserPage
log.info("Creating PlaywrightBrowser bean");
//Added new Optional parameter to the constructor method
return new PlaywrightBrowserSupplier(System.getProperty("browser"),
Optional.of(Boolean.valueOf(System.getProperty("tracing"))));
}
}
Step 3:

Create a package to park your Cucumber hooks, and place related classes in it. In this example class, I am attaching screenshots for each cucumber scenario, and also determining if we need to stop tracing.
import com.microsoft.playwright.Tracing;
import io.cucumber.java.Scenario;
import lombok.extern.slf4j.Slf4j;
import springboot.playwright.cucumber.playwright.PlaywrightBrowser;
import io.cucumber.java.After;

import java.nio.file.Paths;

@Slf4j
public class hooks {
PlaywrightBrowser playwrightBrowser;
hooks(PlaywrightBrowser browser) {
this.playwrightBrowser=browser;
}

@After
public void afterScenario(Scenario scenario)
{
//Capturing screenshot irrespective of whether the test pass or fail
//This approach will ensure there is always evidences for the tests outcome
scenario.attach(playwrightBrowser.captureScreenshot(),
"image/png","screenshot");
log.debug("Attaching full page screenshot, after the scenario");

if(playwrightBrowser.isTracingOptionSet()) {
playwrightBrowser.getPlaywrightBrowserContext().tracing()
.stop(new Tracing.StopOptions()
.setPath(Paths.get("target/traces.zip")));
}
}
}
Step 4:

Create a package to park your Cucumber Junit5 runners:
Pay attention to the annotations used. Also look into the setUp method, from this method we will set system properties, which will be used by the PlaywrightInitializer class, we created earlier.

import io.cucumber.java.Before;
import lombok.extern.slf4j.Slf4j;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("/features")
@ConfigurationParameter(key="cucumber.plugin",
value = "html:target/CucumberTestExecutionReport_Chrome.html")
@Slf4j
public class runnerChrome {
@Before
public void setUp() {
//You could pass these property values using application.properties file also.
//I am writing it in this way to make it easy to understand for those who have
// less technical knowledge
//and willing to learn and experiment more
System.setProperty("browser","chrome");
System.setProperty("tracing", String.valueOf(true));
log.info("Passing browser property to PlaywrightBrowser bean");
}
}
For cross browser execution, create another runner class and set the browser property to a different browser.

Step 5:

Create two package, one for Step Definitions and the other for PageObjects, then add the needed classes.

Step 6:

Create a package for AOP, and park related classes in it:

The @Aspect and @Component annotation makes this class as an aspect. The @Before annotation is defined with a point cut, that will attach to all the methods of pageObjects.PageObject class. The associated JoinPoint will then be used to log the method signatures. It describes how to create common logging. The @Around annotation is defined with  a point cut on Playwright's native package to capture and throw any exception, this is kept as an example to show that we can write custom exception handlers using AOP

import io.cucumber.spring.ScenarioScope;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Slf4j
@Component
@ScenarioScope
public class CommonLogger {
@Before("execution(* springboot.playwright.cucumber." +
"pageObjects.PageObject.*(..))")
public void putCommonLog(JoinPoint joinPoint) {
log.debug("Invoking this method from PageObject class instance: "
+ joinPoint.getSignature().getName());
}

@Around("execution(* com.microsoft.playwright.*.*(..))")
public Object exceptionHandlerWithReturnType(ProceedingJoinPoint joinPoint)
throws Throwable{
Object obj;
obj = joinPoint.proceed();
return obj;
}
}
If you have followed the contents so far and also checked out the associate GitHub repository, then I am sure you have learned step by step how to design a test automation framework, using Playwright, Spring boot, Java, Cucumber and Junit5.

Thursday, August 10, 2023

Why should Test Automation Engineers learn Python?

 Introduction:

This question could be bothering a lot of test automation strategists. Java, C#, Groovy and JavaScript’s are the major programming or scripting languages dominating the test automation space for long. I also explained how a programming languages are chosen for test automation in my previous blog. Checkout this link, here.

So, what’s special about Python? In short, Python is an interpreted language, supports majority of operating systems with .NET framework installed, supporting high level programming, including object-oriented programming and offers numerous choices of powerful libraries for AI and ML, which could be integrated into your automation framework to make it AI/ML enabled. Both PyTest and RobotFramework are very popular, easy to learn, powerful test automation frameworks built on Python. RobotFramework supports a wide range of automation capabilities, like UI, E2E, RPA, Salesforce, Workday, SAP, API and Mobile. PyTest backed frameworks are extensible using Python libraries.

The dilemma on Test Automation Engineers minds:

First is assertiveness about there won’t be any decline on Java/C#/JavaScript automation demands. For example: “I have been working with Java backed test automation from past 10 years, it won’t be easy to acquire Python skills and become proficient in it within few weeks” – such proposition is not always true. Because, you might have worked with only core-java with Selenium / Appium without Spring or Hibernate frameworks, you might have only used TestNg and not Junit, you might used Junit 4 but not Junit 5. You might not try Lombok annotations, running batches with Jsch and reading console logs, connecting to JDBC and executing SQL queries, using JMS for message queues, integration with Swager/pact, JSON/XML parsing. These all are related to Java and required for E2E automation, if you did not work in all these areas in your 10 years journey, and you are not confident on your proficiency in these areas, then you cannot justify your love and commitment with Java.

In today's world, the test automation demands are volatile, recently the curve is going up for C# and JavaScript backed test automation, and it’s going down for java. Python backed automation demand is at equilibrium.

Second, “the syntax is completely different and not easy to remember, not as friendly as java” – such proposition is also not always true. The basic of all programming languages remain same, if you create own mental barriers, then you will never be able to focus on the learning. The Generative AI, can write better code than most of us, if it can learn, then why can’t we?

Our future automation will include more of AI/ML:

As more and more organizations undertaking their AI/ML enablement journeys, our future automation work will involve a lot more testing around AI/ML training data models, predictable complex api’s, big data analytics and numerical analysis. Python is in forefront, and a language of choice for AI/ML programming. So, the more delay we do to appreciate these facts, we will be fallen behind and will repent missing out on the growth opportunities in our career.

Is Python easy to learn:

I would say yes, otherwise in India it should not feature in School level curriculum. From my personal perspective also, yes, it is easy to learn with an open mind, overcoming our preferred programming language biases and psychological resistances, focusing on important semantics which helps us learn by analogy.

Below example explains a simple python program to help you learn by analogy:

#Copy the code in a file and save with .py extension

#Ensure Python 3.11 is installed in your system

#To execute the file, use any command line tool, type: python <filename>.py

#At first, we are creating a class file, with two methods, "init" represent a parameterized constructor method, and "str" represent a special method which instructs how to print the class objects in output, similar to overriding "toString()" method in java. As you can see, there is no need to specify access modifiers, return type, semicolon, curly braces. Python follows strict indention, as long as you are adhering to it, everything works fine. The "brand" and "engineCapacity" are the instance variables.

class Car:

    def __init__(self, brand, engineCapacity):

        self.brand = brand

        self.engineCapacity = engineCapacity

    def __str__(self):

        return f"Brand = {self.brand} and Engine_Capacity = {self.engineCapacity}"

#The "main" method is equivalent to Java/C# main method, but lot simpler in its declaration. The below code is responsible for running the Python program

if __name__ == "__main__":

    #Creating two Car objects

    firstCar = Car("Maruti","1000CC")

    secondCar = Car("Lamborgini","2500CC")

    #Creating a list with the two Car objects created above

    cars= [firstCar, secondCar]

    #Loop construct is similar to "for each" in java,

    for x in cars:

        print(x)

    #The len(list/string) method returns the length of a list or string

    print("Total items in cars list: ", len(cars))

    #Python lists support pop method, which can remove an item from a list with the specified index

    cars.pop(1)

    print("Removed 2nd item from cars array using list pop method, so Lamborgini will no longer be listed")

    for x in cars:

        print(x)

        print("index of "+ str(x) +" is "+ str(cars.index(x)))

    print("Using list append method to add one car to the cars list, the item will be added at last")

    #We can append an item to the tail of a list using the append method

    cars.append(Car("Hyundai","1299CC"))

    for x in cars:

        print(x)

    print("Using list insert method to add one more car to the cars list, at index 1")

    #We can append an item to a given index in a list using the append insert method

    cars.insert(1,Car("Kia","1200CC"))

    print("Present items in cars list")

    for x in cars:

        print(x)

    #Below code iterates a string in a reverse method, there are more simple techniques available

    #to achieve the same

    ms = "Hello"

    for x in range(len(ms)-1, -1, -1):

        print(x)

        print(ms[x])

    #Below code removes duplicate numbers from the list

    my_list = [1, 2, 4, 4, 1, 4, 2, 6, 2, 9]
    for i in range(0,len(my_list)):
        for j in range(i+1,len(my_list)):
            if (j<len(my_list) and (i == my_list[j] or my_list[j] in my_list[0:j])):
                del my_list[j]
    print("The list with unique elements only:")
    print(my_list)

Summary:

In this blog, I just tried to establish a proposition, discussed various aspects around it and cited some  examples. Hope you will find the post useful and share with your friends. Do write comments to share your opinion, it matters most.

Friday, August 4, 2023

What should be the ideal programming language for Test Automation?

 

Introduction:

Before beginning, I would like to thank all readers of my blog, for their appreciation and recognition of the important and unique contents that I have been publishing.

The most trivial question that is often asked to a Test Manager / Test Architect / Automation Architect / Test Lead by a Project Manager and/or a Program Manager, is can we achieve test automation by leveraging open-source test automation tools? The obvious answer is yes, but before submitting a proposal, we need to select an open-source tool and need to consider which programming languages does it supports, and out of those languages which will be more suitable for the project. For example: Selenium Webdriver, it comes with multiple language bindings i.e, java, JavaScript, c#, python, php etc.





Even some low-code licensed automation tools require some knowledge in specific programming languages. For example: Test Complete and Tricentis TOSCA requires some knowledge of JavaScript programming to achieve some test objectives by writing custom code.

Technical landscape of a project / program / portfolio, and its influence:

If you have been working in Test Automation, you must have been monitoring the new job postings (in Glassdoor, indeed, linkedIn etc.) and should have developed an idea about the test automation technical stack the employers are looking for. There are increasing demand for Selenium with C# and Python (PyTest), Playwright using JavaScript (or sometimes Java), Cypress with JavaScript. There is a bit saturated demand for Selenium Java and Selenium JS.

So, how does people in an organization decides on the programming language they want to use for automation? They consider the overall technical architecture and landscape, together with the type of application the project is tasked to build / maintain, the programming language chosen by the development team, the selected unit testing tool, the existing test automation capabilities (skilled personnel across the program/portfolio, automation knowledge-base: test archives, master test strategy for the program/portfolio etc.), learning curve for new hires to the project (including freshers, interns, and experienced hires who needs cross training when there is a skill shortage), availability of reliable open-source libraries for integration (example: for Java, we can integrate a wide array of tools PDFBox, Jsch, Json Schema Validator etc).

The alignment between project, program and portfolio are required to achieve strategic business objectives, maximizing resource utilization, maintaining common framework, curating common body of knowledge, sharing business and technology insights. Ultimately, the core objective is to balance the triple constraints, time, cost and quality.


Unit test frameworks, can they influence the Test Automation tools and its associated programming language selection:

Well, some experts may disagree with me regarding how the selected unit testing tool could influence the opensource test automation tool and its programming language selection. Let me elaborate on this, a modern web application is developed in a multilayered approach by multiple teams who work in parallel in Agile, for example: UI layer, API Gateway, Backend Services (API, GraphQL, MQ/Kafka messaging), DB services, Unix batches. UI layers could be built using Adobe AEM, React, Angular, Vue, Flex UI. Both Cypress and Playwright provides faster unit testing of Web UI on modern browsers using JavaScript. Both these tools could be used for E2E automation as well, given the backend technology stack is npm-node.js. But if the ratio of UI developers is less compared to Backend developers, then to avert skill shortage risk, the project managers and test managers most likely to consider the programming languages that the backend developers use. As they could assist the test automation engineers with some coding if needed. 

So, depending on the back-end technology stack, either java, C#, python or npm-node-js with JavaScript gets selected. And now if you just put a little tap on your head, you will recall Cucumber BDD provides Junit runner which is used for unit testing of Java based codes and used by test automation engineers too. Most importantly, overcome the programming language bias, an objective and logical decision making is required, within the given business context.

Can we break the stereotype:

Until unless you have a solid business case, metrics backed poc report, an approximate roi forecast, can handle criticism, and can negotiate all the odds, my advice will be don’t try it at your workplace. It's difficult to foster organizational change, it needs support from both your team members and the leadership.

If you got confused about what is meant by “stereotype”, then you should ask these type of question to yourself:

  • Why my project does not use Robot Framework with Python for test automation, and rather they are using Selenium Java?
  • Why does my project not use WebdriverIO and using Cypress?

 

Test Automation Strategy for Oracle Forms application running in Citrix servers

  Context : There are many product based applications developed using Oracle Forms and Java thick-client architecture, and most of them are ...