Test Automation Framework (Selenium with Java) — Fear or Code Review and Refactoring (Part 2)

S01E10 of the Test Automation Framework series about everything you’ll need to set up the nice, simple, yet sophisticated framework.

Covered with clear explanations and pretty illustrations.

Sounds like fun? Cool. Now, please, fasten your seatbelts because you’re here for a ride.

S01E01 — What To Automate?

S01E02 — Test Automation Environment and Tools

S01E03 — The First Selenium Test Case

S01E04 — Selenium Foundations Revisited

S01E05 — Page Factory and Elements Related Exceptions

S01E06 — Page Loading Strategies and Waits

S01E07 — Translating JIRA with Selenide (with Exercises)

S01E08 — JIRA, Selenide, Complex SQL, Java Objects with Equals & HashCode (with Exercises)

S01E09 — Code Review and Refactoring (Part 1)

S01E11 — Allure in Action

I was struggling a lot if should I put the Builder Design Pattern in this Test Automation Framework guide. Ultimately, I’ve come up with a conclusion that it can’t hurt to talk about one of the useful approaches to deal with common issues when we’re talking about Java development.

The idea for the Builder Design Pattern comes from the book Design Patterns written by the so-called Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). We’re not going to implement exactly the Builder Design proposed in the book, because it wasn’t designed for Test Automation purposes — just the Builder part is enough for us (as in “we don’t need a Director class”).

Why Builder Pattern?

In essence, Builder Pattern allows you to create Immutable Objects which are safer to use because you simply cannot change them. Thus, they’re Thread Safe. Simply put — if something can’t change you don’t have to worry about it.

ColinD wrote:

It’s kind of tautological, but the main advantage of immutable objects is that they can’t change. When objects can change, you have to think about what might happen with them. You have to think about how, when and why you want to change them. You have to think about what other code in your application might have access to the same object, and what it might change without you knowing. Immutable objects effectively reduce the number (and granularity) of “moving parts” you have to juggle in your system and make your life easier.

StackOverflow Thread about Immutable objects: https://stackoverflow.com/questions/5652652/java-advantages-of-immutable-objects-in-examples

Here’s a guide on how to apply a Builder Pattern in our Test Automation Framework.

Article Model Class

package pl.tomaszbuga.tests.models.article;

import java.util.Objects;

public class Article {

public static ArticleBuilder builder() {
return new ArticleBuilder();
}

private final String title;
private final String authorFullName;
private final String publishDate;
private final String summary;
private final String content;
private final String categoryTagList;
private final String categoryTitleList;

Article(final String title,
final String authorFullName,
final String publishDate,
final String summary,
final String content,
final String categoryTagList,
final String categoryTitleList) {
this.title = title;
this.authorFullName = authorFullName;
this.publishDate = publishDate;
this.summary = summary;
this.content = content;
this.categoryTagList = categoryTagList;
this.categoryTitleList = categoryTitleList;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Article articleDetails = (Article) o;

if (!title.equals(articleDetails.title)) return false;
if (!authorFullName.equals(articleDetails.authorFullName)) return false;
if (!publishDate.equals(articleDetails.publishDate)) return false;
if (!Objects.equals(categoryTagList, articleDetails.categoryTagList)) return false;
if (!Objects.equals(categoryTitleList, articleDetails.categoryTitleList)) return false;
if (!Objects.equals(summary, articleDetails.summary)) return false;
return Objects.equals(content, articleDetails.content);
}

@Override
public int hashCode() {
int result = title.hashCode();
result = 31 * result + authorFullName.hashCode();
result = 31 * result + publishDate.hashCode();
result = 31 * result + (summary != null ? summary.hashCode() : 0);
result = 31 * result + (content != null ? content.hashCode() : 0);
result = 31 * result + (categoryTagList != null ? categoryTagList.hashCode() : 0);
result = 31 * result + (categoryTitleList != null ? categoryTitleList.hashCode() : 0);
return result;
}

}

Article Builder Class

Implementation in DbDataProvider Class

getArticlesListByCategoryId()

getArticleDetails()

Implementation in ArticleDetailsPage Class

getArticleDetailsFromPage()

Implementation in ArticlesPage Class

getArticleListFromPage()

As you can see it’s quite a similar syntax, but there is quite a change — Article created with the Article.builder().build() method is an Immutable Object, so we can worry not about the Thread Safety now.

Now, once we got the concept of the Builder Pattern, let’s clean the code even more, okay?

Let’s assume your QA Lead says something like:

— I saw your Pull Request. It’s looking good! Anyhow, I wanted to ask you for a little favor. I saw that you’re not using Lombok and there’s plenty of boilerplate code. Let’s implement Lombok library, so that we can get rid of it, okay? Thanks!

You better get used to learning about plenty of new technologies daily, especially if you’re in the entry development position.

Lombok — what’s that?

According to the official website of Project Lombok:

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.
Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

Yeah, so basically most of the stuff that we did in the last 3 episodes of Test Automation Framework could’ve been written as Lombok’s annotation 😂

Let’s learn by practice. Firstly — we need to update the pom.xml with a new dependency.

Once we have that, let’s try out one of the annotations — @Log4j2

We can get rid of the constructors in our BasePage and PageWithSubtitle classes, as we only needed them to pass the Class to the BasePage constructor to set up the LogManager.

BasePage after implementing Lombok

PageWithSubtitle after implementing Lombok

We also had to change the LOGGER constant to a log constant in every Page Object Model as this is the name of the Logger instance stored in the default Lombok’s Log4j2 configuration (according to Lombok’s documentation).

Also, we already know what the Builder Pattern is all about. We can safely remove the ArticleBuilder class entirely and make a little rework of the Article class.

Article class after implementing Lombok

As you can see we’re using two different Lombok annotations: Builder and EqualsAndHashCode. We no longer need a constructor, as the Builder logic is being managed by the Lombok. Also, as the equals() and hashCode() methods are implemented, we can remove our implementation from the Article class.

Notice that even though we’re using the Builder annotation we still need to use some custom-made methods for Builder setters. To overwrite the default Lombok’s Builder methods we simply define the inner static class called ArticleBuilder and we implement the methods as they were defined in the original setters.

Here, you can learn a little bit more about the Custom Setters in Lombok: https://www.baeldung.com/lombok-builder-custom-setter

Side note: While preparing for this section of the article, I’ve stumbled upon one ugly bug with the JWT token persistence logic. It would clear the JWT token in cookies each time the User opens/refreshes a page. I’ve spent an entire day fixing this bad boy, and I’m proud to state that the fix is already merged to the master branch on the GitHub Repository of the Ultimate Stack Developer app.

Our code is becoming less error-prone, more readable, and overall easier to maintain. Let’s keep refactoring!

Take a look at the outline of the Article Details Page Test Case flow:

First of all, we should focus on determining if all of those steps are crucial for the proper test execution. Here are two simple points that may be helpful in this case.

1. Is this part of the application tested anywhere else?

When you’re writing your Test Cases, Scenarios, and whatnot, chances are that you’re reusing the steps in some form (e.g. Precondition in the Xray JIRA Plugin).

In the automation world, it’s much easier to set up the Test Environment as we’d like — hence, it’s easier to remove some irrelevant elements from the Test Steps. Imagine that you have to test the behavior of the pagination if there are 500 articles available in the database. You’d rather want to create some type of API Automation to automatically populate the database instead of manually clicking through each of the UI layers of the application 500 times.

Let’s see in detail an example of one of the most common skippable steps of any Test Automation Framework — le User Authentication.

Almost every app available on the market uses some kind of login mechanism. For instance — the Ultimate Stack Developer’s Home Page introduces a yellow button that does the authentication part automatically.

We click the yellow button. The magic is taking place backstage.

  1. Login REST API call is performed to verify the username and password combination (and compare it with the combination in the Database).
  2. Then the Token returned in the HTTP Response is being saved in the Cookies. It’s later reused for each GraphQL call as an Authorization header.
It’s not relevant to what are we’re dealing with in this article, but two of the GraphQL API calls are performed to preload all the required data after logging in. This is a performance trick I’ve come up with when I was designing the Ultimate Stack Developer; that’s why there’s an animated loader — so the User is entertained in the meantime.

What we can do with this knowledge? For starters — we can entirely skip the log-in process in Test Cases other than those inside the HomePageTests class.

We’re going to cover the “how to do it?” part later in this article.

2. Do I really need to do this via UI?

Do we really need to click through every page? Look at the list below and think about how it’s related to checking if the Article Details page is displayed correctly.

  1. .checkIfCategoriesIsLoaded()
  2. .clickFirstAvailableCategory()
  3. .checkIfArticlesPageIsLoaded()
  4. .hoverOverGoToArticleButton()
  5. .clickGoToArticleButton()

The answer is it’s not related at all — we don’t need to do any of these things just to get to the Article Details page.

Here’s what the process would look like after implementing Auth API and skipping all of the irrelevant Test Steps in our Test Automation Framework.

Okay, that’s enough of the theory for now. Let’s dive into the ocean of possibilities that the Rest Assured library provides us with.

Rest Assured (or how they call it — REST Assured) is a library that makes it’s easy to test the REST API. That’s it.

We need to update the pom.xml file once more, to add the Rest Assured to our project.

Now we need a class to handle the Auth API calls. Let’s create a new UserAuth.java within pl.tomaszbuga.utils package.

Below, as usual, you can find a code snippet, that we’re going to use to give a Rest Assured a spin.

Let’s read it bit by bit.

We initialize the token as an empty String — we’re using it later in the generateTokenIfNeeded() method.

This method is self-explanatory — we’re going to cover generateTokenIfNeeded() and addCookie() methods below.

Here’s a catch, though — you have to open any page to add a Cookie to the WebDriver, so that’s why we’re using the open() method before adding the Token to Cookies.

This method simply checks if the token has been already generated — if not, then we’re going to use the setter method that invokes the getJwtToken() method and assigns the result to the token variable.

addCookie() method instantiates a new Cookie object with “token” as a name and assigns the Response from the Rest Assured API call as a value.

Then we’re using the Selenium’s own WebDriver().manage().addCookie() to add a new Cookie to the current WebDriver.

Here’s a video of the same call done within the Postman app. The Content-Type, Content-Length, and Host headers are taken care of automatically by the Postman and Rest Assured alike.

I wanted to keep things simple, so we’re not using any fancy libraries to handle the JSON formatting — it’s a simple one-line String.

Rest Assured allows us to use it in many different ways. As we have to handle only one method I’ve decided to not use RestAssured.baseURI and RestAssured.port — instead baseUri() is being invoked in the methods chain.

You can find more details and ways to handle the Rest Assured requests here: https://github.com/rest-assured/rest-assured/wiki/Usage#specifying-request-data

Here’s what our Test Cases look like after implementation of the UserAuth class.

HomePageTests

HomePageTests class is not affected by this change, as tests cover the Login behavior thus don’t require any API calls.

CategoriesPageTests

ArticlesPageTests

ArticleDetailsPageTests

There you have it! Also, I wanted to be sure that’s a more efficient way to handle automated tests, so I’ve decided to run some performance tests. The results are quite a surprise.

For my old laptop (Mid 2012 MacBook Pro with an old 2.5 GHz Dual-Core Intel Core i5) Auth API approach was almost the same — performance-wise, as the Non-Auth API.

For my wife’s brand new laptop (MacBook Air with the state-of-the-art Apple’s M1 processor) Auth API approach was winning with results that were 1–2 seconds better than Non-Auth API ones.

Ultimately — there is no loss (or it’s negligible) in performance, hence if we’re dealing with much more complicated apps the gain in the Test Suite overall performance can (and most probably — will) be significant.

Also, if you’re dealing with rather small apps, like side projects — tests won’t perform at a higher pace, so this type of update can be considered overkill.

You’re about to press the Merge button to merge your feature git branch to the master branch when your QA Lead sends this article about Java 17 being a new Long Term Support version and asks you on Slack to do this simple update. Also, he mentions Text Blocks, which we could use to deal with SQL queries.

Without further ado — here’s an excerpt from Text Blocks docs with a template we’re going to use to store our SQL queries.

String formatted(Object... args)

This method is equivalent to String.format(this, args). The advantage is that, as an instance method, it can be chained off the end of a text block:

First of all, as I’d like to keep things simple and clean, let’s create a new SqlQueriesProvider class inside the pl.tomaszbuga.utils.database package.

package pl.tomaszbuga.utils.database;public abstract class SqlQueriesProvider {protected static String getCategoryTitlesQuery() {
return """
SELECT title FROM category;
""";
}
protected static String getArticlesListByCategoryIdQuery(String categoryId) {
return """
SELECT article.title,
concat(author_first_name, ' ', author_last_name) AS author_full_name,
article.publish_date,
string_agg(category.tag, ', ') AS category_tag_list,
string_agg(category.title, ', ') AS category_title_list
FROM category,
article_category,
(
SELECT article_id AS aid
FROM article_category
WHERE category_id = %s
) AS ac
INNER JOIN article ON article.id = ac.aid
WHERE article.id = article_category.article_id
AND category.id = article_category.category_id
GROUP BY 1, 2, 3;
""".formatted(categoryId);
}
protected static String getArticleDetailsQuery(String articleId) {
return """
SELECT article.title,
article.publish_date,
CONCAT(author_first_name, ' ', author_last_name) as author_full_name,
article.summary,
article.content
FROM article
WHERE article.id = %s;
""".formatted(articleId);
}
}

As you can see SQL queries are much more readable now. Notice how we’re using the .formatted() method with the %s placeholder inside the queries instead of the String concatenation.

Once, we’ve got the SqlQueriesProvider in place — we can refactor the DbDataProvider class.

package pl.tomaszbuga.utils.database;import pl.tomaszbuga.tests.models.article.Article;import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import static pl.tomaszbuga.utils.database.DbConnector.getConnection;
import static pl.tomaszbuga.utils.database.SqlQueriesProvider.*;
public class DbDataProvider {public static List<String> getCategoryTitles() {
List<String> titleList = new ArrayList<>();
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(getCategoryTitlesQuery())) {
while (resultSet.next()) {
String categoryTitle = resultSet.getString(1);
titleList.add(categoryTitle);
}
} catch (SQLException e) {
e.printStackTrace();
}
return titleList;
}
public static List<Article> getArticlesListByCategoryId(String categoryId) {
List<Article> articleList = new ArrayList<>();
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(getArticlesListByCategoryIdQuery(categoryId))) {
while (resultSet.next()) {
Article articleFromList = Article.builder()
.title(resultSet.getString(1))
.authorFullName(resultSet.getString(2))
.publishDate(resultSet.getString(3))
.categoryTagList(resultSet.getString(4))
.categoryTitleList(resultSet.getString(5))
.build();
articleList.add(articleFromList);
}
} catch (SQLException e) {
e.printStackTrace();
}
return articleList;
}
public static Article getArticleDetails(String articleId) {
Article articleDetails = null;
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(getArticleDetailsQuery(articleId))) {
while (resultSet.next()) {
articleDetails = Article.builder()
.title(resultSet.getString(1))
.publishDate(resultSet.getString(2))
.authorFullName(resultSet.getString(3))
.summary(resultSet.getString(4))
.content(resultSet.getString(5))
.build();
}
} catch (SQLException e) {
e.printStackTrace();
}
return articleDetails;
}
}

Now everything’s cleaner, organized, and most important — easier to maintain.

That would all for now.

We’ve got quite a complex Test Automation Framework, but there’s still one thing that’s missing — reporting. In the next episode, we’re going to implement the Allure so that we’ve got everything in place before jumping into the DevOps territory with the CI/CD pipelines of Jenkins and CircleCI.

What we’ve learned in this episode?

  • How to apply Builder Design Pattern to Java Object
  • What is Lombok and how to use it
  • What is Rest Assured and how to use it
  • How to increase the performance of Test Cases with API calls
  • Why update to Java 17 can be useful for managing SQL queries

In case of any questions (I believe there can be one or two of those) — feel free to post them in the comments section below.

All the best,

Tomasz Buga, SDET

www.tomaszbuga.pl

Sources:

GitHub Repository available at: https://github.com/n4bik/test-automation-framework/tree/Fear-AuthAPI

All illustrations made by Tomasz Buga

--

--

Software Development Engineer in Tests. Passionate about programming. Experienced, former employee of the insurance industry. Graphic designer by choice.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tomasz Buga

Tomasz Buga

121 Followers

Software Development Engineer in Tests. Passionate about programming. Experienced, former employee of the insurance industry. Graphic designer by choice.