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

S01E09 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)

S01E10 — Code Review and Refactoring (Part 2)

S01E11 — Allure in Action

In the previous episode, we’ve finished writing Test Cases for JIRA tickets (credentials to access the JIRA board can be found in the S01E07 — Translating JIRA to Selenide section).

If you couldn’t come up with your solution for the Test Cases, or you’d like to follow the Test Automation Framework tutorial from this episode — feel free to clone the GitHub repository with the code from here: https://github.com/n4bik/test-automation-framework/tree/Magic-ClientTest

In the real world after writing each Test Case you’d commit your changes and push them into a specific git branch. After that, you’d most probably create a Pull Request (if you’re using GitLab) or a Merge Request (if you’re using GitHub) to proceed with merging your code branch to the master/develop (depends on your company’s policies/practices).

Your work is done as of now. It’s time for your lead or co-worker to perform a so-called Code Review. Simply put it’s a process of checking the changes made to the code by another person that has a fresh perspective (and has more knowledge in case you’re a junior) so that your team can be sure that your changes are compliant with the framework requirements, code styling, usage of the proper classes, clean code practices and so on. It’s also a great opportunity for knowledge sharing and growing together as a team.

Here’s a simulation of a Code Review I’ve made to make you familiar with the process and what you could expect. I’ve created a new Merge Request for the Magic-ClientTest branch to do so.

Foreword:

I won’t be rewriting step-by-step guides of the implemented methods. If you’d like to learn more about why those Page Object Models are written like that, please follow two previous episodes where I explained their prototypes (ergo simple Test Case implementations) in detail: S01E07 & S01E08.

Also, there’s still going to be a lot of repeated code, as I didn’t feel the need to refactor it more, so I will be explaining only one occurrence of the method. In case I’d miss something, or something is not explained enough — please leave a comment in the comments section below the article.

Firstly, let’s focus on the Page Object Models comments.

Within the pl.tomaszbuga package I’m going to create a new package called pom so that we can have a clear structure of the project.

As a first step, I would like to create a BasePage.java.(more on the BasePage: S01E04 — Selenium Foundations Revisited). For clear structure let’s add the pom.utils package, as the BasePage is not an actual Page Object Model.

It’s quite a straightforward basic Page Object Model. We’re going to share the baseUrl, so we don’t need to rewrite that part of an URL for each of the classes that will extend the BasePage.

Secondly, we’re using the Java Generics Wildcard to pass the Class as a parameter, so that we could initialize the LOGGER variable for a specific page. To clarify — if we’d initialize the LOGGER variable for only BasePage, like this:

This would be a result:

When we’re using Generics and applying the LOGGER for each class separately, this is the output:

As you can see it’s easier to read the logs, as we know what Page Object is currently in the charge of the WebDriver.

Once we’ve got the BasePage ready, let’s create the HomePage.java within the pom package. Below you can find a code for the file.

Let’s break it down, shall we?

Here we’re extending the HomePage so that we can use the baseUrl and LOGGER.

This is how we can create locators using the Selenide. Please, keep in mind this is only a Locator, not the WebElement per se. It’s pretty much the same if we’d use Selenium’s FindBy annotation.

This is where we’re passing the HomePage.class for further use with the LOGGER.

We’re creating a so-called Fluent API this is why we’re returning the HomePage object after opening the page. We’re using the LOGGER so that we can track what’s happening with test logs. Then we’re opening the baseUrl. At last, we’re using the return this; to return the current Object which is a HomePage.

The last step is to create a click the yellow button method. To do that we’re simply logging the action we’re performing, we’re using the .click() method on the yellowButton variable. After clicking the yellow button the User will be redirected to the Categories page so that we need to return a new Categories page Object.

That’s it for the Home Page — Page Object Model.

Next stop — Categories Page Object Model. At first, I would like to create a new utility Page Object Model for the pages that contain a subtitle. On the pictures below you can see those areas with the “Please select…” text within — that’s the subtitle.

To do that, let’s create a new class PageWithSubtitle. Code is very simple as it contains only one SelenideElement that we want to share between classes.

As you can see it extends BasePage, and the purpose of the PageWithSubtitle is just an additional layer of abstract so that’s our code is more readable and reusable.

Once we’ve done that we can create a new CategoriesPage within the pom package.

Again, let’s focus on the small chunks of the code.

We’re extending the PageWithSubtitle object. Essentially we’re extending both PageWithSubtitle and BasePage because the first one is a subclass of the latter one.

We’re creating the categoriesUrl with a concatenation of the baseUrl and categories suffix.

This is another example of how we can substitute Selenium’s FindBy annotation. It’s just a List<WebElement> but written in Selenide.

For this one, I’ve decided to go with the double-check if the page is loaded. That’s because I would like to use Selenide’s .should() methods instead of using assertions. It’s just more convenient to do, and if the page won’t load the .shouldHave() method will fail with a neat error message, screenshot, and page logs. As for my taste — that’s more than enough.

This is a kind of self-explanatory method. We’re using the ElementsCollection titlesFromPage and we’re performing the .shouldBe() and .texts() methods on it to receive a List of String values.

We’re getting the first index of the titlesFromPage (again — the first index is the 0, not 1) and clicking the Category button that it contains. Finally, we’re returning a new Articles Page Object Model and this is what we’re going to cover next.

This one (Articles Page) is finally something more complex, where I can show you how Selenide can make a code more friendly and concise.

That’s the biggest one so far, so let’s just get into details.

Here we’re defining articlesUrl and locators. You’re probably wondering why not write those as a SelenideElement. That’s a good question. It’s because I couldn’t find a way to use a SelenideElement within nested lambda functions that we’re using to extract the Article details from the page.

I know it’s not pretty, but still, you have to remember — when you’re developing tests (or any type of an application) there’s probably going to be plenty of places where code is not perfect. Add deadlines to the equation and that’s the point where you simply have to decide where’s the line between an overengineering and a clean code.

That’s part which I like the most about Selenide. Because we don’t need to use the PageFactory we can keep everything in one-liners instead of FindBy annotations per each element, which would make the top section of your Page Object class just a huge chunk of annotations and WebElements which is not easy to maintain and read.

This is self-explanatory — we’re getting categoryId from the URL. I wrote more on this function (with an actual example) here: S01E08 — JIRA, Selenide, Complex SQL, Java Objects with Equals & HashCode (with Exercises)

This part took the most time of the refactoring, as it’s quite large. As for the basics and step-by-step guide of what’s happening here, please go back to the previous episode (S01E08).

Mostly I’ve turned the pure String values into variables and replaced $() with find(), so it’s more readable.

Despite that, one of the minor changes that I applied here is the parseListIntoString method that is being used in both article.setCategoryTagList() and article.setCategoryTitlteList().

To do that I’ve created a new StringUtils class within the pl.tomaszbuga.utils package. It has only one static method that you can see in the snippet below.

In the end, I’ve created some methods to cover the hovering over the element and make sure that the proper value is being displayed.

Again, it’s quite self-explanatory, but even though I wanted to pinpoint one thing, that may bring some confusion. I decided to make both checkIfCategoryBadgeTitleDisplayed() and checkIfGoToArticleSidebarDisplayed() methods return void because there weren’t any appliances to those methods other than a simple assertion.

Only one Page Object Model is left to implement.

This fella is quite easy, as most of the methods and variables we have already covered in the previous Page Object Models.

Article Details page doesn’t have the subtitle element, hence we don’t need to extend PageWithSubtitle class.

I’ve decided that I would like to check if every element is loaded within the checkIfArticleDetailsPageLoaded() method because the related test is based on simple getting values (text or innerHTML) so I wanted to make sure that everything will be loaded before that.

I’ve created the getArticleDetailsFromPage() method within the ArticleDetailsPage class because I forgot to do that earlier and it’s better to keep the separation of concerns if you’re writing easy to maintain Test Automation Framework.

Every get() method is simply getting the text value of the WebElement. The getArticleContent() is somewhat special, because we need to get the innerHTML value. We need to do that so that the comparison of the articleContent from the database and the webpage will be the same in terms of formatting because the database value has HTML tags present.

getArticleContent() using .getText() method

getArticleContent() using .innerHTML() method

I won’t be doing an overview of the tests code, because it’s a Page Objects Models method chained together. A very detailed explanation can be found in this article in the previous sections, and two previous articles S01E07 & S01E08.

HomePageTests Refactored

CategoriesPageTests Refactored

ArticlesPageTests Refactored

package pl.tomaszbuga.tests.client;import org.apache.commons.collections4.CollectionUtils;
import org.testng.Assert;
import org.testng.annotations.Test;
import pl.tomaszbuga.pom.ArticlesPage;
import pl.tomaszbuga.pom.HomePage;
import pl.tomaszbuga.tests.models.article.Article;
import pl.tomaszbuga.utils.DbDataProvider;
import java.util.List;public class ArticlesPageTests {

@Test
public void verifyArticlesListWithDataBase() {
HomePage homePage = new HomePage();
ArticlesPage articlesPage = homePage
.openHomePage()
.clickYellowButton()
.checkIfCategoriesPageLoaded()
.clickFirstAvailableCategory()
.checkIfArticlesPageLoaded();
List<Article> articlesListFromDb =
DbDataProvider.getArticlesListByCategoryId(articlesPage.getCategoryIdFromUrl());
Assert.assertTrue(CollectionUtils
.isEqualCollection(articlesListFromDb, articlesPage.getArticleListFromPage()));
}
@Test
public void verifyTooltipDisplayOnBadgeHover() {
HomePage homePage = new HomePage();
homePage
.openHomePage()
.clickYellowButton()
.checkIfCategoriesPageLoaded()
.clickFirstAvailableCategory()
.checkIfArticlesPageLoaded()
.hoverOverCategoryBadge()
.checkIfCategoryBadgeTitleDisplayed();
}
@Test
public void verifyOpenArticleSidebarExpandOnHover() {
HomePage homePage = new HomePage();
homePage
.openHomePage()
.clickYellowButton()
.checkIfCategoriesPageLoaded()
.clickFirstAvailableCategory()
.checkIfArticlesPageLoaded()
.hoverOverGoToArticleButton()
.checkIfGoToArticleSidebarDisplayed();
}
@Test
public void verifyThatGoToArticleButtonRedirectsToArticleDetails() {
HomePage homePage = new HomePage();
homePage
.openHomePage()
.clickYellowButton()
.checkIfCategoriesPageLoaded()
.clickFirstAvailableCategory()
.checkIfArticlesPageLoaded()
.clickGoToArticleButton()
.checkIfArticleDetailsPageLoaded();
}
}

ArticleDetailsPageTests Refactored

One comment in the Code Review was different — it was about implementing a Fluent API or a Builder pattern to the Article model class.

I’ve written this comment with a specific intention in mind. I wanted to show you two different approaches to the same issue.

As you already could’ve seen in the previous section when we were applying the Fluent API and chaining our methods together, it’s quite an effective solution to organize your code and make the whole process a lot quicker.

Let’s refactor our Article model in the same way.

The only thing we need to do is to change the type of the returned Object within each Setter and add the following line at the end:

After changes your Setters should look more or less like this:

Notice that I’ve also applied the StringUtils.parseListIntoString() static method to both setCategoryTagList() and setCategoryTitleList().

Now, we can make use of the Fluent API within every class that uses the Article class.

DbDataProvider.java

getArticlesListByCategoryId() method

getArticleDetails() method

ArticlesPage.java

getArticleListFromPage() method

ArticleDetailsPage.java

verifyThatArticleDetailsAreDisplayedCorrectly() method

getArticleDetailsFromPage() method

We’ll cover the Builder Design Pattern in the next episode along with the other refactoring ideas, that you can find useful.

At last, we can run all the tests to make sure they’re working as expected.

To do that we’re going to use the IntelliJ built-in Run Configuration for TestNG.

Click on the Add Configuration… button on the header bar

Within the modal window press the plus icon and select the TestNG from the dropdown menu

Name it Regression (or type whatever name that suits you). For the Test kind select Suite, as we want to use our testng.xml file which contains Test Suite.

In the Suite field press the folder icon and select the testng.xml file location or type manually.

Select the In single module radio button as we’re building quite a simple, single-module Java application.

Make sure that the Use classpath of the module field is set to test-automation-framework and that JRE is set to Java 11.

That’s it, now you can press the OK button.

Now you can run the entire Test Suite with the Play button located next to the Run Configuration dropdown menu.

After a while, you should see a green alley of passed tests. Neat!

What we’ve learned in this episode?

  • What is Code Review and how it works
  • How to apply Page Object Models with Selenide
  • How to use Java Generics to refactor the Logging
  • How to create a Fluent API
  • How to create an additional layer of abstraction for better maintainability
  • How to use methods chaining and Lambda expressions with Selenide
  • How to create a StringUtils class to deal with the common String related issues
  • What is a difference between .getText() and .innerHTML() methods
  • How to refactor Test Cases with Fluent API
  • How to apply Fluent API to Java Object
  • How to use the IntelliJ Run Configuration to set up the Test Run

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-FirstCodeReviewRound

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.