Intro
Recently I participated in Craft Conf 2022 which is one of the world's biggest developer conferences. There were more than 70 talks from top-notch experts from all around the world. The event was held in Budapest the capital of Hungary. Thanks to this I was able to visit the conference on-site.
Being a test automation engineer for no surprise my main focus was on listening to talks about various quality assurance topics. There were only a few talks in this category so I had the chance to listen to all of them. One of them was Fabrice Bernhard's fascinating talk about the Dantotsu method with which you can radically improve the quality of your software. I have already written my summary of that talk which you can find on my blog.
Being a Hungarian I definitely wanted to listen to the talks of the Hungarian speakers and to my luck I found one talk about test automation patterns by the Hungarian Gspr Nagy. Jackpot.
Gspr Nagy works as an independent coach, trainer, and test automation expert helping teams to implement Behaviour Driven Development (BDD). Gspr's talk about test automation patterns inspired me to write this article. In the first part of this writing, I will highlight some of the topics from Gspr's speech and in the second part I will talk about a tool Selenideium Element Inspector which I developed to automate one of the most repetitive tasks in end-to-end testing and I will also talk about 2 patterns I use in test automation.
A Brief History of Design Patterns
Have you heard about the term GoF? Not familiar? What about Gang of Four? I am pretty sure by this point most of the developers know what will I am talking about here.
It is the book Design Patterns: Elements of Reusable Object-Oriented Software. This book is kind of a bible for developers. The book has been published in 1994 which means 28 years ago. (Oh crap, I am too old) Despite being so ancient this book is still very actual many of the patterns are used by developers. By now these patterns became part of programming languages and libraries. However, Christoper Alexander came up with the concept of design patterns in 1977, this book was which made them popular.
The term design pattern was borrowed from architecture. In architecture, you do not start from zero when you would like to build something but you use previously proven patterns by which you make sure the building will function as expected.
If you are using design patterns you can progress faster, have a more reliable and maintainable code, and you also have a common language among developers. You do not have to explain the code all the time you just say I used the Builder pattern here or the Factory pattern there.
Test Automation Patterns
Test automation code should be a first-class citizen, which means it should have the same quality as production code. How can we achieve for our production code to have great quality? We can follow coding standards, do reviews, use design patterns, and of course write tests for our code. That raises a very interesting topic which was explained in Gspr's talk in a pretty funny way: if tests should have the same quality as production code, then we need tests to make sure it has high quality, but those tests should also be the same quality as production code which results in an endless loop.
In order to break that loop, we are typically applying exploratory testing to our tests. Many testing schools like TDD say you should see your test failing first and move forward only afterward. By seeing the tests first failing you can make sure your test can actually catch some error. That is exactly what we do in TDD.
As mentioned above, using design patterns can improve the quality of your code but the good news is you can also use test automation patterns to improve the quality of your tests.
Tests are typically composed of some sort of usual tasks for example for a complex automated test you have to reset the database to a known state. Collecting these usual repetitive tasks and their solutions into a test automation pattern knowledge base can help in not inventing the wheel every time. These patterns can be useful on a team level, some of them on a company level and some of them can be also useful outside your company.
In the upcoming part, I write about a repetitive task I have automated and I also introduce two design patterns. Let's see them one by one.
Automating Locator Creation
As a test automation engineer, my daily job includes writing both API and E2E tests depending on the actual need of the project. During writing E2E tests one of the most time-consuming tasks is to find the best locator of a certain web element.
As Gspr mentioned test automation patterns usually emerge from automating repetitive tasks and then reusing the same method again and again. This is actually what I did by developing the Selenideium Element Inspector (SEI) Chrome Extension.
Let me explain the task with an example. I have a complex form where you have to enter numerous personal information. For simplicity let's say the first name and last name are mandatory and the others are optional. I would like to test a happy path where I only fill the mandatory fields. For simplicity let's say it is just a part of the test and we won't do any assertion (more about that will come later).
So what is the task I should do? I have to determine a locator for the first name text field, the last name text field, and also for the submit button.
In an ideal world each web element has an ID or a class based on which every element can be located easily, but let's be honest very often that is not the case. Cause of this I have to come up with a locator based on ID, name, className, tagName, linkText, partialLinkText, CSS or xPath. Or at least those are the locators that you can use with Selenium Webdriver which is one of the most widely used frameworks for E2E test automation and also the one we use in combination with Selenide (more on that later) in our team.
At this point, I open Chrome dev tools and start to inspect the web elements. Let's say my first name text field has no id, but it has a class that seems unique. As I am not sure about that I open the console to run the selection in the form of a CSS selector just to realize there are other elements with the same class. Maybe it is not the best example, but if you did automation for a while I am pretty sure you encountered such problems.
So I look at the DOM again and come up with a CSS selector with which I can select the exact element. Then I repeat the procedure for the last name and also for the submit button. Of course, you can become pretty quick in writing locators still it is quite repetitive.
This repetitive task is not specific to my team, to Selenium but it is a task that has to be done by everyone writing E2E tests. Of course, some framework provides support for recording user interactions even Selenium has Selenium IDE, but I wanted a faster solution and also I did not want to leave the final choice for a framework as it can often happen that an id or class which it selects is an autogenerated value which will change on each page load.
In the end, I decided to develop a Chrome Extension with which you can automatically generate each relevant unique locator for a web element with a single click. You can not just generate the locator, but a complete line of copy-pastable code. You just have to look at the console and copy-paste the guaranteed unique selectors to your favorite IDE.
In the first run, I only implemented the code generation for Selenium Java and Selenide but I had some discussions with my colleagues from other teams during which I learned they are using TestCafe and Squish so I decided to support those frameworks too. By now I've added support for most of the other widely used frameworks including Playwright and Cypress and Selenium Javascript, C#, and Python too.
After I have developed the plugin I realized there are other similar tools like SelectorsHub, but I still prefer using my plugin as you can generate everything with a single click.
Of course, there is a lot of space for improvement one idea on my mind is to automatically generate page object models from the selected web elements. Please, feel free to share your thoughts in the comments section.
You can read more about the plugin including full source code here, here and here and you can download it from the Chrome Webstore.
The Page Object Model (POM) and Tester Pattern Duo
During the talk, Gspr encouraged us to share test automation patterns that we use, so I decided to share two patterns I use in end-to-end test automation. However, Gspr gave his suggestion on how to document a pattern I did not want this article to be too formal so I decided to simply explain these patterns with my own words, but Gspr is working on a book with his colleagues which will contain many test automation patterns in a structure very similar to the GoF book. I am really looking forward to reading that book.
In order to introduce the patterns let me sketch up a simple scenario.
We have a login page with two text fields a username and a password and of course, we have a log-in button. After a successful login, the user is navigated to its profile page. In our test, we will test the positive scenario when we enter valid credentials and the user successfully logs in. After the login, we will verify the user is really logged in by making sure the user's name is visible and a logout button appears on the screen.
The Framework
As far as I know, Selenium Webdriver is the most widely used framework for web application testing. It is open-source, easy to learn, and supports many programming languages, browsers, and platforms. Well, in this article I won't use Selenium but I will use Selenide instead. Let me explain why.
Selenide is an open-source framework built on top of Selenium WebDriver whose main purpose is to make test automation simple. As it is based on Selenium WebDriver it also means we get the full feature set of Selenium WebDriver.
In addition to that, it has simpler syntax, a concise fluent API, additional selectors, simple configuration, profiler, and more. If you are interested in a more in-depth comparison just read my article Selecting an End to End Testing Framework - Selenium or Selenide?. Anyone who is familiar with Selenium WebDriver or other test automation frameworks can easily understand the Selenide tests.
Despite having the above-mentioned advantages Selenide is not as well known as it should be, so I would like to grab the opportunity to draw attention to it. However, in this example I will use very little from its functionality you can learn more about it in one of my previous articles..
The Test
For simplicity, I will use id-based locators and hardcoded test data as they are not relevant for introducing the patterns. So let's see the example code:
@Test
public void successfulLogin() {
open("https://mywebapp.com/");
$(byId("username")).sendKeys("johndoe");
$(byId("password")).sendKeys("pass");
$(byId("login-button")).click();
$(byId("logout-button")).shouldBe(visible);
$(byId("user-display-name")).shouldHave(text("John Doe"));
}
Selenide handles timeouts and the web driver in the background so we do not have to deal with them which makes our life much easier and we can also forget about the StaleElement exception. We have our test so it is time to do a little refactoring by introducing the Page Object Model.
Page Object Model (POM)
If there is only one test automation design pattern a developer knows I am pretty sure it is the POM. Using the POM pattern we create a corresponding class (Page Object) for each page in which we encapsulate all the web elements belonging to that page. In addition to that, we can also encapsulate higher-level interactions like the user login in our case.
Why we should use POM? Just imagine you have a web element that you use at multiple locations in your test code. If the element changes you have to modify all occurrences. In simple terms, we have a much more maintainable code by using the POM pattern, and also we can make our tests more readable by encapsulating multiple-step interactions into methods with meaningful names. Clean code is not just important for production code but also for tests.
Let's see our code for the login page:
public class LoginPage {
private final By usernameInput = byId("username");
private final By passwordInput = byId("password");
private final By loginButton = byId("login-button");
public void login(String username, String password) {
$(usernameInput).sendKeys(username);
$(passwordInput).sendKeys(password);
$(loginButton).click();
}
}
And also for the profile page
public class ProfilePage {
private final By logoutButton = byId("logout-button");
private final By userDisplayName = byId("user-display-name");
public SelenideElement getLogoutButton() {
return $(logoutButton);
}
public SelenideElement getUserDisplayName() {
return $(userDisplayName);
}
}
And the refactored test:
@Test
public void successfulLogin() {
LoginPage loginPage = new LoginPage();
ProfilePage profilePage = new ProfilePage();
open("https://mywebapp.com/");
loginPage.login("johndoe", "pass");
profilePage.getLogoutButton().shouldBe(visible);
profilePage.getUserDisplayName().shouldHave(text("John Doe"));
}
Well at first look it might seem overkill to do this as from the 6 lines long test we made another 6 lines of test and 2 extra classes but imagine we have many login scenarios using the same elements again and again or having webpages with dozens of elements. From the page object, you can easily get an overview of the different elements belonging to a page. The two page objects can be moved to a before test initialization and if we have only login scenarios in this file then the webpage opening can be moved too, resulting in this code:
@Test
public void successfulLogin() {
loginPage.login("johndoe", "pass");
profilePage.getLogoutButton().shouldBe(visible);
profilePage.getUserDisplayName().shouldHave(text("John Doe"));
}
This code reflects our intention much more as it is clear we are doing login, but there is one problem with it. It does not reflect our intention regarding what we would like to test and here comes the Tester pattern in the picture.
The Tester Pattern
If you are not familiar with the name Tester pattern it is not your fault. I made up this name for the pattern as I have not heard about it before. I see very little chance that nobody else came up with this pattern before so if you know its original name then please share it with us in the comment section.
So what is our intention here? We would like to verify the user logged in. Let's make it clear by introducing the Tester pattern:
public class LoginTester {
private final ProfilePage profilePage;
public LoginTester(ProfilePage profilePage) {
this.profilePage = profilePage;
}
public void verifyUserLoggedIn() {
profilePage.getLogoutButton().shouldBe(visible);
profilePage.getUserDisplayName().shouldHave(text("John Doe"));
}
}
And the test after moving the tester initialization into a pretest initialization code:
@Test
public void successfulLogin() {
loginPage.login("johndoe", "pass");
tester.verifyUserLoggedIn();
}
If we are taking into account the initialization code then we have a clear given-when-then structure which makes our test code easily understandable and maintainable.
That was my intro to the Tester pattern in a nutshell. I use it as it makes my life easier but I am pretty curious what you think about it. Do you find it useful? Or do you see some drawbacks? Share your thoughts in the comment section.
Thanks for staying with me up till this point. I am pretty sure you also have some test automation design patterns in your bag so do not hesitate to share them in the comment section.
Top comments (0)