Design Patterns for Test Automation Solutions: Part 2 -JavaScript/Typescript

Kostiantyn Teltov
14 min readMay 30, 2023

Hello colleagues. I hope you are doing great.

For many months I was thinking about extending one of my previous articles about Test Design Patterns by adding JavaScript/TypeScript usage examples. Finally, I had a chance to do it. This article contains more research than the previous one. As a bonus, I’ve added some additional design patterns that may or may not be used again, but it’s still interesting to know about them.

Why JavaScript?

The answer is simple. JavaScript/TypeScript has become very popular not only in the developer community but these days a lot of QA’s use it to design test automation solutions and write automated tests/scripts.

What is the difference?

Conceptually, there is no difference. All the principles are the same. The difference is only in how it can be implemented in JavaScript/Typescript languages. That’s why I want to call this article the second part. Hope it will also help me to adapt more to JavaScript/TypeScript implementations.

Let’s go!

Creational Patterns

Creational design patterns are a set of design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable for the situation. The same as in C# or Java, most of the creational design patterns may be used during the designing of Test Automation Solutions.

Singleton

The Singleton pattern ensures that only one class instance can be created and provides a global point of access to that instance.

In this example, we define a APIClient class that implements the Singleton pattern. The getInstance static method ensures that only one instance of the APIClient class is created and returns that instance. Public methods such as get and post can then be used to make API requests.

There are more similar things we can implement using Singleton, like DataBase or Configuration classes.

Anyway, you should remember, if you’re not careful, the Singleton pattern can also make your code less flexible and more difficult to refactor. Be sure to weigh the benefits and drawbacks of using the Singleton pattern in your test automation solution and only use it when it makes sense for your specific use case.

Factory method

The Factory Method pattern provides an interface for creating objects without specifying their concrete classes.

Let’s imagine we have a few TestData classes, both implement the TestData interface. One Test Data will be used for SignUp and another for log-in.

To avoid the creation of specific classes we can create TestDataFActory class with a method that creates specific test data implementation.

Ultimately, we can call the same method specifying the different parameters. A different Test Data will be created depending on the specified parameter.

There are much more examples. One of them is the creation of PageFactory. I’m not going to dive deeply into PageObjects implementation. We will discuss them a little bit later.

By using the Factory Method pattern in your test automation solution, you can make your code more flexible and easier to maintain, especially when you need to create different types of tests dynamically based on certain conditions.

Abstract factory

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Let’s think we need to work with both SQL and non-SQL databases.

In this example, we define two different families of related objects: SQL and MongoDB database objects. We also define a DatabaseFactory an interface that provides factory methods for creating both DatabaseConnection and DatabaseQuery objects.

Then implement the two families of objects using the SqlDatabaseFactory, MongoDatabaseFactory, SqlDatabaseConnection, SqlDatabaseQuery, MongoDatabaseConnection, and MongoDatabaseQuery classes. These classes provide concrete implementations for creating SQL or MongoDB DatabaseConnection and DatabaseQuery objects.

SQL family:

Mongo family:

In the usage section, we create instances of the SqlDatabaseFactory and MongoDatabaseFactory classes, and use their factory methods to create DatabaseConnection and DatabaseQuery objects. This allows us to create database objects from either family of databases, without having to specify their concrete types.

If you want to add support for a new database type, you would just create new classes implementing the DatabaseConnection, DatabaseQuery and DatabaseFactory interfaces.

Builder

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create various representations.

One of the options is to create complex API requests in a more modular and maintainable way.

In this example, we define an ApiRequest object that comprises a URL, method, headers, and an optional body. We also define an ApiRequestBuilder interface that provides methods to set the different properties of an ApiRequest object and a BasicApiRequestBuilder class that implements this interface.

We then use the BasicApiRequestBuilder class to create a complex ApiRequest object in a more modular and maintainable way. We chain the different configuration methods on the builder object and finally call the build method to create the ApiRequest object. We then use the fetch function to send the API request with the configured options.

Using the Builder pattern in your test automation solution can help you create complex test objects in a more modular and maintainable way. You can easily add new flavors or variations of test objects by implementing new builder classes that conform to your interface.

Prototype

The Prototype pattern creates a new object by cloning an existing object, thus avoiding the overhead of creating objects from scratch.

Let’s imagine you need to create TestData objects. But you don’t want to repeat filling in the same data each time.

In this example, we define an TestData interface that provides some properties for test data objects and a BasicTestData class that implements this interface. We also define a clone method on the BasicTestData a class that allows us to create a new instance of the class by cloning the existing one. We use the Partial<TestData> type to indicate that not all properties need to be provided.

In the Usage section, we create an original TestData object and clone it with a partial test data object that changes the name property. The clonedData an object has the same properties as the originalData object, except for the updated name property.

Using this implementation of the Prototype pattern in your JavaScript or TypeScript test automation solution can allow you to clone existing test objects with the option to make changes. This can be useful when you want to create new test objects that are similar but differ in some properties.

Structural Patterns

Structural design patterns are a category of design patterns in software engineering that focus on relationships between objects and how objects can be composed into larger structures. They are useful for designing software systems that are flexible, extensible, and maintainable.

Page Object

Page Object is a well-known design pattern in Test Automation that can be applied in JavaScript/TypeScript test automation solutions to simplify and maintain automation tests, and make them more modular, reusable, and readable.

A Page Object is an object-oriented class that represents a web page or section of a page and contains page-related logic and elements as well as its related actions or methods. The main idea behind the Page Object pattern is to define a single source of truth for every page of the application, where all related operations to that page can be accessed and modified easily.

In this example, we create a Page Object for the Login page of our application, with the Page instance passed in as a constructor argument. We also declare and initialize the private instance variables for the login-related elements of the page, userNameInput, passwordInput, and loginButton.

In the loginWith method, we use Playwright's API to interact with the page elements and perform the login action.

In the async self-invoking function, we launch a Chromium browser and create a new context and a new page. We navigate to the login page, create an instance of the LoginPage class, and call the loginWith method to log in to the application with the given username and password.

Finally, you can add your assertion code here to confirm whether the login was successful.

This example is just a simple demonstration of how to use Page Objects in Playwright with TypeScript. By leveraging Page Objects in your Playwright tests, you can improve the modularity, maintainability, and readability of your test code.

Adapter

The Adapter pattern can be used in Test Automation to convert the interface of a class into another, which can prove useful if you need to integrate with external libraries, frameworks, or modules that do not directly work with your testing framework.

Let’s imagine, we have an existing LegacyLoginPage class that has an incompatible interface with our LoginPage interface. The LegacyLoginPage authenticates with a username and password, whereas our LoginPage expects an object with username and password fields.

To make the LegacyLoginPage class compatible with our LoginPage interface, we wrote a LoginPageAdapter class that implements the LoginPage interface and uses the methods of the LegacyLoginPage class to authenticate the user.

Now, we can use the LoginPageAdapter class to log in to the application with the object-based LoginPage interface, without having to change the LegacyLoginPage code.

In summary, the Adapter Test Design Pattern in JavaScript/TypeScript test automation solutions simplifies testing by providing a unified API to test various components of an application. By creating an adapter that bridges incompatible interfaces, you can easily integrate different components in a way that they can cooperate seamlessly. This pattern makes it easier to test different parts of the system with the same testing tools or frameworks and saves time and effort in the long run.

Facade

The Facade pattern can be used in Test Automation when you want to provide a simplified interface to a larger body of code, such as a class library, making it easier for test developers to use and maintain.

In this example, we have a complex test case that logs in to the application and verifies the greeting text on the home page. Instead of writing the test case directly in our self-invoking async function, we create a Facade class, the ApplicationFacade, to encapsulate the complex logic.

The ApplicationFacade class uses the LoginPage and HomePage classes to provide a simplified interface for logging in and getting the greeting text. It abstracts the complexities of interacting with the various components of the application and provides a more concise and readable way to perform the test.

Now, we can use the ApplicationFacade class to execute the test case without worrying about the underlying details of the LoginPage and HomePage classes.

Proxy

The Proxy Test Design Pattern is a structural pattern that provides a surrogate object that acts as a placeholder for another object. The proxy object controls access to the real object, allowing it to be created only when needed.

In JavaScript/TypeScript test automation solutions, the Proxy Pattern can be applied to limit the interaction with an object or class, providing additional functionality without changing the object’s code.

In this example, we have an IIpAddress interface and a concrete IpAddress class that implements it. The IpAddress class retrieves the user's IP address using a REST API call to https://ipinfo.io/json.

We then have a IpAddressProxy class that also implements the IIpAddress interface and acts as a proxy, controlling access to the actual IpAddress object. The IpAddressProxy class checks if the user's IP address has already been cached and retrieves it if it hasn't. Otherwise, it returns the cached IP address.

Finally, we have a self-invoking async function where we create an instance of the IpAddressProxy class and make two requests for the user's IP address. The first request results in a call to the actual IpAddress object, while the second request retrieves the cached IP address.

In summary, the Proxy Test Design Pattern is useful in Test Automation solutions when you want to control access to an object, add security or performance checking, or delay object creation. The pattern provides a proxy object that behaves like the actual object but adds additional functionality without changing its code.

In my opinion, you will rarely use this pattern. Anyway, it is nice to know about it.

Decorator

The Decorator pattern can be used in Test Automation to add or modify the existing behavior of the method without change of the method itself.

In this example, we have an ILoginPage interface and a concrete LoginPage class that implements it. The LoginPage class provides a simple login implementation that logs in to the user using a username and password.

We then have a LoginPageDecorator a class that also implements the ILoginPage interface and decorate the LoginPage class. The LoginPageDecorator adds a message before and after the actual login process, providing additional behavior without changing the code of the LoginPage.

Finally, we have a self-invoking async function where we create instances of the LoginPage and LoginPageDecorator classes and call the loginWith method on both of them. The first call to the loginWith method logs in the user using the simple LoginPage, while the second call logs in the user using the decorated LoginPageDecorator.

In summary, the Decorator Test Design Pattern is useful in Test Automation solutions when you want to add behavior, logging, or exception handling to an object without changing its code. The pattern provides a decorator object that encapsulates the original object and adds additional functionality.

Once more, in my opinion, you will rarely use this pattern too.

Behavioral Patterns

Behavioral Design Patterns are a category of design patterns in software engineering that focus on communication and interactions between objects to help design complex, coherent behaviors in your system. These patterns focus on how objects communicate with one another, how they collaborate to achieve common goals, and how they interact in complex systems.

State pattern

The State pattern is widely used in test automation to handle situations where the software system being tested has different states, and each state requires different actions to be performed.

In this example, we have a LoginPage class that represents the login page of a web application.

The LoginPage class has a state variable that starts with a LoggedOutState.

We also have a LoginPageState abstract class that defines the interface for all the states that LoginPage can have.

Each state class responds differently to the setUsername(), setPassword(), and clickSignInButton() methods. When a method is called, the state changes to a new state using the setState() method.

This allows us to easily handle different states in the LoginPage class without having to write complex conditional statements. The LoginPage class only needs to know about its current state, and the state classes handle the rest.

Now you may access the state and understand the status of our Page.

Maybe it is a rare case when you need to use it. But you should definitely try to consider it in a case when you need to control some system states. Maybe when you need to build some conditional tests.

Command pattern

The Command Pattern can be useful in Test Automation Solutions when you need to encapsulate requests or actions as objects. This approach allows you to parameterize clients with different requests to perform at other times, as well as support undoable operations. For example, when executing test cases, each test case can be encapsulated as an object and executed based on its parameters.

First, let’s create an interface that will be a contract to all our API commands:

Then, let’s create API methods implementing this interface. For our example, I will just create UserCreate and UserGet functions. In real life, it may have more methods.

To execute these commands, you can create a CommandManager the class that maintains a list of commands and executes them in order.

In this example, the CommandManager maintains a list of commands and provides methods to add commands, execute them, and undo them. When you call commandManager.execute(), it will loop through each command, call the execute() method, store the results in an array, and add the executed commands in reverse order to the undo stack. When you call commandManager.undo(), it will grab the last executed commands from the undo stack and call their undo() method to reverse their effects.

Now you can create your test case by defining each command and adding it to the CommandManager

Here we initialize our command manager and add our REST API commands.

Finally, we can execute commands in order

Personally, I have never used this pattern before. But looking at this now, in some cases using the Command pattern in this way makes testing applications more readable, maintainable, and organized.

In the end

The research is over. Sorry for the long read. In my apology, I will say that I left only those patterns that I believe can be used in the design of test automation solutions. I found it very interesting to try out the new design patterns, even though it’s a rarity when I get to use them. Hope this article will show you more possibilities and you will start using some of these patterns. You may build something Great! Good luck!

--

--

Kostiantyn Teltov

From Ukraine with NLAW. QA Tech Lead/SDET/QA Architect (C#, JS/TS, Java). Like to build testing processes and help people learn. Dream about making indie games