Design Patterns in Test Automation — Part 1

Anji Boddupally
4 min readSep 25, 2023

--

Knowledge of Java programming is required to understand the contents of this article.

I have come across some of the Design Patterns that can be used in Test Automation.

Factory Pattern: Factory pattern is a creational pattern, which takes care of instance creation by taking some condition into consideration. Hence, it decouples the object construction from the client.

Let's write a small test to start the FirefoxDriver.

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

import io.github.bonigarcia.wdm.WebDriverManager;

@TestInstance(Lifecycle.PER_CLASS)
public class GoogleTest {

private static final String EXP_TITLE = "Google";

private WebDriver driver;

@BeforeAll
public void setUp() {

WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
}

@Test
public void testGooglePageTitle() {

driver.get("https://www.google.com");
Assertions.assertThat(driver.getTitle()).isEqualTo(EXP_TITLE);

}

@AfterAll
public void cleanUp() {
driver.quit();
}
}

Now, I want to run above test against chrome browser. Then what options I do have? Well, I can change the code, replace FirefoxDriver with ChromeDriver. But, for what price? what if I have hundreds of such tests.

So, here we can make use of the amazing Factory pattern to get the driver instance based on some input (browser name) and my test class does not need to worry about Driver creation.

Let's write a Factory class to get the actual driver instance.

package com.anji.sel.core;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.safari.SafariDriver;

import io.github.bonigarcia.wdm.WebDriverManager;

public class WebDriverFactory {

public static WebDriver getDriver() {
String browser = System.getProperty("browserName", "chrome");
return getMeTheDriver(browser);
}

private static WebDriver getMeTheDriver(String browser) {

return switch (browser.toLowerCase()) {
case "chrome" -> {
WebDriverManager.chromedriver().setup();
yield new ChromeDriver();
}

case "firefox" -> {
WebDriverManager.firefoxdriver().setup();
yield new FirefoxDriver();
}

case "ie" -> {
WebDriverManager.iedriver().setup();
yield new InternetExplorerDriver();
}

case "safari" -> {
WebDriverManager.safaridriver().setup();
yield new SafariDriver();
}

default -> throw new IllegalArgumentException("No such browser exists: " + browser);
};
}
}
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.openqa.selenium.WebDriver;

import com.anji.sel.core.WebDriverFactory;

@TestInstance(Lifecycle.PER_CLASS)
public class GoogleTest {

private static final String EXP_TITLE = "Google";

private WebDriver driver;

@BeforeAll
public void setUp() {
System.setProperty("browserName", "firefox");
driver = WebDriverFactory.getDriver();
}

@Test
public void testGooglePageTitle() {
driver.get("https://www.google.com");
Assertions.assertThat(driver.getTitle()).isEqualTo(EXP_TITLE);
}

@AfterAll
public void cleanUp() {
driver.quit();
}
}

Now to run the tests against chrome, we can simply set the browerName system property to chrome or do not setup anything at all as browserName system property defaults to chrome. If we want to set the property via command line we do it as below

mvn test -Dtest=xxx.xxx.GoogleTest -DbrowserName=safari

Remember, this is possible because ChromeDriver, FirefoxDriver and other drivers implements WebDriver (Ex: ChromeDriver extends RemoteDriver & RemoteDriver implements WebDriver) and during the runtime based on parameter we get the actual driver instance. It is called the Runtime Polymorphism, we will discuss this topic another article.

Factory Pattern in Test Data Creation:

Let's assume, we are testing a POST api which takes a book entity as input and verifies all the fields are present before it persists in the database. If we miss any of the property or set any of the property to null value, we get error message related to that field.

POST: https://boddupally.com/v1/book

{
"title" : "Effective Test Automation",
"author" : "Anji Boddupally",
"price" : "45.00"
}
import static org.assertj.core.api.Assertions.assertThat;

import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;

import org.junit.jupiter.api.Test;

import com.anji.rest.RestClient;
import com.anji.rest.RestResponse;
import com.anji.book.Book;

public class BookTest {

@Test
public void testErrorMessageForEmptyBookTitle() {

Book bookWithEmptyTitle = new Book("", "Anji Boddupally", 45.00d);

RestResponse response = RestClient.post(bookWithEmptyTitle);

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Book title cannot be empty");

}

@Test
public void testErrorMessageForNullBookTitle() {

Book bookWithNullTitle = new Book(null, "Anji Boddupally", 45.00d);

RestResponse response = RestClient.post(bookWithNullTitle);

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Book title cannot be null");

}

@Test
public void testErrorMessageForEmptyAuthorName() {

Book bookWithEmptyAuthor = new Book("Effective Test Automation", "", 45.00d);

RestResponse response = RestClient.post(bookWithEmptyAuthor);

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Author name cannot be empty");

}

@Test
public void testErrorMessageForNullAuthorName() {

Book bookWithNullAuthor = new Book("Effective Test Automation", null, 45.00d);

RestResponse response = RestClient.post(bookWithNullAuthor);

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Author name cannot be null");

}
}

There is a lot of repetitive code and also if we want this Book object with nullable fields or empty string values in another Test Classes, we need to construct the same Book objects multiple times. Rather constructing this Book Test Data in BookTest class, let's create a BookTestDataFactory which gives us various Book objects.

package com.anji.book;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BookTestDataFactory {

public static Book bookWithEmptyTitle() {
return new Book("", "Anji Boddupally", 45.00d);
}

public static Book bookWithNullTitle() {
return new Book(null, "Anji Boddupally", 45.00d);
}


public static Book bookWithEmptyAuthor() {
return new Book("Effective Test Automation", "", 45.00d);
}

public static Book bookWithNullAuthor() {
return new Book("Effective Test Automation", null, 45.00d);
}
}

Now our test class looks like below

import static org.assertj.core.api.Assertions.assertThat;

import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;
import static com.anji.student.BookTestDataFactory.bookWithEmptyAuthor;
import static com.anji.student.BookTestDataFactory.bookWithNullAuthor;
import static com.anji.student.BookTestDataFactory.bookWithNullTitle;
import static com.anji.student.BookTestDataFactory.bookWithEmptyTitle;

import org.junit.jupiter.api.Test;

import com.anji.rest.RestClient;
import com.anji.rest.RestResponse;


public class BookTest {

@Test
public void testErrorMessageForEmptyBookTitle() {

RestResponse response = RestClient.post(bookWithEmptyTitle());

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Book title cannot be empty");

}

@Test
public void testErrorMessageForNullBookTitle() {

RestResponse response = RestClient.post(bookWithNullTitle());

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Book title cannot be null");

}

@Test
public void testErrorMessageForEmptyAuthorName() {

RestResponse response = RestClient.post(bookWithEmptyAuthor());

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Author name cannot be empty");

}

@Test
public void testErrorMessageForNullAuthorName() {

RestResponse response = RestClient.post(bookWithNullAuthor());

assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);

assertThat(response.getResponse()).isEqualTo("Author name cannot be null");

}
}

If we want to BookTest Data in any other test class, we can simply use these objects from BookTestDataFactory.

Still there is a duplicate code in our Test Class — BookTest, so let's refactor a bit to make it more tidy.

import static com.anji.student.BookTestDataFactory.bookWithEmptyAuthor;
import static com.anji.student.BookTestDataFactory.bookWithEmptyTitle;
import static com.anji.student.BookTestDataFactory.bookWithNullAuthor;
import static com.anji.student.BookTestDataFactory.bookWithNullTitle;
import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import com.anji.rest.RestClient;
import com.anji.rest.RestResponse;
import com.anji.student.Book;

public class BookTest {

@ParameterizedTest
@MethodSource("getBookTestData")
public void errorMessageTest(Book book, String expMessage) {
RestResponse response = RestClient.post(book);
assertThat(response.getResponseCode()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getResponse()).isEqualTo(expMessage);
}


private static Stream<Arguments> getBookTestData() {
return Stream.of(
Arguments.of(bookWithEmptyTitle(), "Book title cannot be empty"),
Arguments.of(bookWithNullTitle(), "Book title cannot be null"),
Arguments.of(bookWithEmptyAuthor(), "Author name cannot be empty"),
Arguments.of(bookWithNullAuthor(), "Author name cannot be null")
);
}

Happy learning & happy testing :)

--

--