Unleashing the Power of TypeScript Generics in Test Automation

Unleashing the Power of TypeScript Generics in Test Automation

Simplify Your Tests with TypeScript Generics

For creating adaptable and reusable code in a test automation suite, TypeScript generics is a strong and practical tool. Without having to make distinct versions of the component for each type, they enable you to construct components that can function with a range of different types.

In this post, we'll examine in more detail how TypeScript's generics function and how they might be applied to test automation.

Typescript Generics

Generics are a means to make reusable parts that function with various kinds in TypeScript (and other programming languages). In your code, you can use them to provide placeholder types that can be filled in with actual kinds when the code is executed.

Here is a straightforward TypeScript example of a generic function:

function toArray<T>(val: T): T[] {
  return [val];
}

In this example, the toArray() function is defined as a generic function that takes in a value of type T and returns an array of that type. The placeholder type T is specified using angle brackets (<T>).

To use this function with a specific type, you can specify the type when you call the function. For example:

const numArray = toArray(10); // => [10]
const strArray = toArray('happy new year'); // => ['happy new year']

In these examples, the placeholder type T is filled in with the concrete types number and string, respectively.

Generics vs Any Type

any type in TypeScript is a type that can represent any value. A value of type any can be assigned to a variable of any other type since it is a top type that is a supertype of all other types.

Here's an example of how you might use the any type:

let val: any;

val = 5;
val = 'hello';
val = true;

The val variable in this illustration is of type any, which means that any type of value may be assigned to it.

Generics, on the other hand, are a technique to make reusable parts that function with various kinds. In your code, you can use them to provide placeholder types that can be filled in with actual kinds when the code is executed.

Here's an example of how you might solve the problem induced by the any type using generics:

function toUpperCase<T extends string>(val: T): T {
  return val.toUpperCase();
}

let val: string;

val = 'helloworld';
console.log(toUpperCase(val)); // => 'HELLOWORLD'

val = 2023;
console.log(toUpperCase(val)); // error: Argument of type '2023' is not assignable to parameter of type 'string'.

In this example, the toUpperCase() function is defined as a generic function that takes in a value of type T (which must be a string or a subtype of string) and returns a value of type T. This ensures that the function can only be called with values of type string or a subtype of string, which has a toUpperCase() method.

If you try to call the toUpperCase() function with a value of a different type (e.g., a number), the TypeScript compiler will raise an error, because the val the argument is not of type string.

Generics in Test Automation

Because they enable you to write adaptable, reusable code that can be used with a range of different types, generics are especially helpful in test automation. By eliminating the need to write several versions of a function or component for each type, you can save time and effort.

Problem: You are creating a test suite to make sure the login form component is operating properly. The login form takes a username and password and, in the event that the login is unsuccessful, shows an error message. Writing a function that verifies the error message shown on the login form and returns a boolean result indicating whether the message is correct is required.

Making distinct routines for each type of error message that the login form can display is one way to deal with this issue. For instance:

function isCorrectInvalidUsernameError(errorMessage: string): boolean {
  return errorMessage === 'Invalid username';
}

function isCorrectInvalidPasswordError(errorMessage: string): boolean {
  return errorMessage === 'Invalid password';
}

function isCorrectAccountLockedError(errorMessage: string): boolean {
  return errorMessage === 'Your account has been locked';
}

However, if the login form generates a variety of error messages that need to be examined, this strategy may soon become cumbersome. The creation of a single, reusable function using generics that can examine any error message would be a preferable alternative:

function isCorrectError<T>(errorMessage: T, expectedErrorMessage: T): boolean {
  return errorMessage === expectedErrorMessage;
}

The error message and intended error message can then be passed to this function, which will return true if the error message is valid and false otherwise. For instance:

const invalidUsernameError = 'Invalid username';
const invalidPasswordError = 'Invalid password';
const accountLockedError = 'Your account has been locked';

isCorrectError(invalidUsernameError, 'Invalid username'); // => true
isCorrectError(invalidPasswordError, 'Invalid password'); // => true
isCorrectError(accountLockedError, 'Your account has been locked'); // => true
isCorrectError(invalidUsernameError, 'Invalid password'); // => false

Instead of having to construct a different function for each sort of error message, you can utilize generics in this way to create a single, reusable function that can be used to check any error message. Your test suite may become more adaptable and simple to manage as a result.

Here's another example problem statement that might benefit from the use of generics is,

Problem: You are building a test suite to verify that an application's search component is working correctly. The search function returns a list of results, and you need to write a function that checks the list of results to see if it contains a specific item.

One approach to solving this problem would be to create a function that takes in a list of results and a specific item, and returns a boolean value indicating whether the item is in the list:

function isInResults<T>(results: T[], item: T): boolean {
  return results.includes(item);
}

A boolean value indicating whether the item is in the list is returned by this function, which is passed a list of results of type T and an item of type T. Instead of having to construct distinct functions for each type, you can utilize generics in this way to create a single, reusable function that can be used to check for items of any kind.

For example:

const results = ['Selenium', 'Appium', 'Taiko'];

isInResults(results, 'Appium'); // => true
isInResults(results, 'Webdriver'); // => false

const numbers = [1, 2, 3, 4];

isInResults(numbers, 2); // => true
isInResults(numbers, 5); // => false

In addition to creating reusable functions and components, generics can also be used to create more complex types in TypeScript. Here are a few advanced use cases for generics in test automation:

  1. Defining reusable test helper functions: Generics can also be used to define reusable test helper functions so that they can be used with a range of different types. The generic convertToStringT> function, for instance, accepts a value of type T and outputs a string representation of that value. The creation of test output that is understandable by humans is then made simpler by using this method to convert values of any type to strings.

  2. Creating type-safe mocks: Making type-safe mocks that imitate the behavior of the external dependencies might be helpful when testing code that uses external services or APIs. Generics can be used to build type-safe mocks that can operate on a wide range of types. As an illustration, you might develop a generic MockServiceT> type with a getData() function that returns a T-type value. The code that depends on a genuine service that provides values of different types might therefore be tested using this mock service.

  3. Defining reusable assertion functions: Additionally, assertion functions that are reusable and flexible can be defined using generics. For instance, you could design a generic assertEqualT> function that accepts two values of type T and checks to see if they are equal. It would then be possible to compare values of any type using this method, making it simpler to create reusable and type-safe assertions for your test suite.

  4. Creating flexible page object models: A page object model can be used to represent the components and functionality of a page or application under test in a web or UI test automation suite. Generics can be used to build adaptable page object models that function with a wide range of distinct element kinds. Although the page object model has several flaws w.r.t adopting the single responsibility principle, etc unfortunately it is one of the widely used models in the test automation world.

To write a single generic login function that returns the appropriate page based on the origin page in TypeScript, you can use generics to specify the type of page that the function should return. Here's an example of how you might do this:

interface SearchPage {
  search(query: string): void;
}

interface HomePage {
  navigateToProfile(): void;
}

function login<T extends SearchPage | HomePage>(page: T): T {
  // Code to log in to the application
  return page;
}

const searchPage = login(new SearchPage());
searchPage.search('search query');

const homePage = login(new HomePage());
homePage.navigateToProfile();

Note: The point I want to make here is regarding the use of generics in a normal page object model. There are better methods to write the same piece of code than what is shown above.

In this example, the SearchPage and HomePage interfaces represent the search page and home page of the application, respectively. Each interface has a method that is specific to that page (search() for SearchPage and navigateToProfile() for HomePage).

The login() function is defined as a generic function that takes in a page of type T (which can be either SearchPage or HomePage) and returns a page of the same type. The type parameter T is constrained to the types SearchPage and HomePage, which means that the function can only be called with pages of these types.

When the login() function is called with an instance of SearchPage or HomePage, the placeholder type T is filled in with the concrete type of the page (SearchPage or HomePage, respectively). This ensures that the login() function returns the appropriate page type based on the origin page.

You may create a more adaptable and reusable test automation suite that can handle a range of various types and scenarios by using generics in these sophisticated ways. You can save time and effort by doing this, and your test suite will be easier to manage in the long term.

Happy Learning :star:

Did you find this article valuable?

Support Srinivasan Sekar by becoming a sponsor. Any amount is appreciated!