14 Test Automation Best Practices

Maria Golubeva
JAGAAD Digital Solutions
7 min readFeb 22, 2024

--

Automated testing has become an essential part of modern software development, enabling teams to streamline their testing efforts and deliver high-quality software faster.

In this article, I’ll walk you through 14 simple yet powerful practices to help you get the most out of your test automation efforts. From test case design to execution and analysis, these practices aim to optimize the testing process and enhance the overall quality of software products.

I’ll illustrate these best practices using examples from automation with TypeScript Cypress.

1. Start with a strategy

Before diving into test automation, it’s essential to have a clear plan or strategy in place. Define clear objectives and goals for your test automation efforts. Identify what to automate and what not to automate based on feasibility.

It also means talking to the people who know the application best, for example, PM, BA or UI/UX designer — team members who understand how it works and what’s essential to users. You can also get insights from the QA department, who can share their experience from past projects and suggest whether to stick to a proven strategy or come up with a new one tailored to the needs of this specific project.

2. Use test sets wisely

Organize your tests into groups based on topics, functionalities, or pages of the application. For example, you might have a set for testing login functionality, another for testing payment features, and so on. This makes it easier to manage and run tests efficiently, especially as your test suite grows larger.

As an example, let’s consider the “Booking flow” functionality (test suite), which includes tests such as “Book ticket for adult” and “Book tickets for adult and child”.

And here is an example of usage describe() to define the “Test set” and it() to define the “Test case” in Cypress:

describe('Booking flow', () => {

it('Book ticket for adult', () => {
//test scenario code
});

it('Book tickets for adult and child', () => {
//test scenario code
});

3. Describe test scenarios comprehensively

Describe every step and expected results of a test before writing any code. It’s like making a plan before building something — you want to know exactly what you’re going to do before you start. By detailing each step in advance, it becomes easier to write the actual test code later on because you’ve already defined what needs to be tested and how.

Example of test case for automation

4. Write maintainable test cases

Write test cases that are easy to understand and maintain. Use descriptive and meaningful names for tests and test components, take a time to optimise methods to be reusable. Follow defined in your company coding standards and conventions. Below, I’ll share some ideas on code optimization.

5. Add identifiers to HTML elements

By adding a data-testid attribute to HTML elements in the front-end code, you’re essentially labeling them for testing purposes. It helps automated tests accurately identify and interact with these elements, even if there are modifications to the HTML layout. Sometimes, QA Engineers don’t have access to the FE code repository, and in this case, you have to request data-testid to your developers.

Examples of data-testid for elements in HTML

Example of usage data-testid in Cypress:

addNotes(value: string) {
cy.findByTestId('notes')
.type(value);
}

6. Use preconditions and postconditions

Preconditions and postconditions help us preparing for a test and cleaning up afterward. Before running a test, we need to ensure that the system is in the right state for the test to be conducted (preconditions). After the test is finished, we need to reset the system or perform any necessary cleanup to leave it ready for the next test (postconditions). Methods like Before, After, BeforeEach, and AfterEach help us automate these setup and cleanup tasks, ensuring that our tests are reliable and independent.

Example of usage before() , beforeEach(), after() hooks in Cypress:

describe('Booking flow', () => {

before(() => {
createExcursion(excursionTypes.ticket.disneyland);
});

beforeEach(() => {
cy.visit('/');
loginPage.navigateToAmazonLoginPage();
amazonLoginPage.signIn();
homePage.isDisplayed();
})

after(() => {
deleteExcursion(excursionTypes.ticket.disneyland);
});

it('Book ticket for adult', () => {
//test scenario code
});
});

7. Use API calls to automate tasks, saving time and avoiding flakiness

This involves utilizing API endpoints, for example, to create necessary test data sets and configure the state of user accounts before running tests, and to clean up or reset this data afterward. Using API calls not only ensures consistency but also saves time by automating repetitive tasks.

Example of usage request() method in Cypress:

export function createExcursion(excursionType: string) {
cy.request({
method: 'POST',
url: 'https://www.test',
headers: { 'X-API-KEY': 'test key' },
body: {
type: excursionType,
},
});
}

8. Apply Page Object pattern appropriately

With this pattern, each page of your application is represented by a separate object in your code. This makes your tests more organized and easier to maintain, as changes to the UI can be managed within the corresponding page object. By using the page object pattern, your test code becomes more readable and scalable.

It’s reasonable to use the Page Object pattern in test automation when:

  • The application has a complex UI with multiple pages or elements that require frequent interaction in tests.
  • There is a need to reuse elements or actions across multiple tests or test suites.
  • You anticipate frequent changes to the UI, and you want to centralize UI-related changes in one place to make maintenance easier.

It may not be necessary to use the Page Object pattern when:

  • The application has a simple UI with only a few pages or elements that don’t require much interaction in tests.
  • You’re creating one-off or ad-hoc tests that don’t need to be maintained or reused in the future.
  • You’re under tight time constraints and need to quickly create tests without focusing on long-term maintenance or scalability.

Here is an article written by Gleb Bahmutov about Page Object and App Actions usage in Cypress: https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions

9. Apply inheritance for shared page elements

Some pages can be built from one template. Instead of duplicating code for shared elements across multiple page objects, we can create a base page object that contains the common elements. Then, we can create child page objects for specific pages that inherit from the base page object. This helps to make our test code more modular and organized.

Example of page inheritance

10. Group shared components for reusability

It’s important to recognize when certain elements appear on multiple pages like headers, footers, or menus.

By identifying these shared components and keeping them in a separate files, as in the point above, we can avoid repeating the same code across different page objects. Instead, we write methods to interact with these shared elements just once. This approach not only saves time and effort but also makes our test code more efficient, maintainable, and reusable.

11. Develop reusable helper methods

It’s beneficial to create helper methods for tasks that are frequently performed across different tests, like authorization. These helper methods encapsulate the logic for performing these actions, making them reusable across multiple tests.

Folders structure of Cypress project

12. Log test details for easier debugging

It’s helpful to log important information at various stages of test execution. These logs can include details such as test steps, input data, expected outcomes, and actual results. Logs provide valuable information for troubleshooting and debugging test failures, helping to find the root cause of issues and resolve them efficiently.

Example of usage log() method in Cypress:

 addNotes(value: string) {
cy.findByTestId('notes').type(value);
cy.log(`The "${value}" value is added to the notes`);
}

13. Organize test data into dedicated files

It’s beneficial to store various types of test data in separate files dedicated to specific categories. These files can contain information such as application content, locators for identifying elements on the UI, input values for test scenarios, URLs of web pages, and credentials for authentication. The practice of organizing test data in this manner and exporting it allows for the reuse of data and facilitates easy updates by centralizing it in a single file. Instead of modifying the data in multiple places within the tests, you can make changes in one location, ensuring consistency and efficiency across the testing process.

Example of .json file with data:

{
"adult": {
"name": "TestName",
"surname": "TestSurname",
"email": "test@test.test",
"number": "+375333333333",
},
"child": {
"name": "ChildTestName",
"surname": "ChildTestSurname",
}
}

14. Do the regular maintenance

Regularly review and update automated tests to ensure they remain relevant and effective. Ignoring maintenance can make your tests less effective over time. Make changes as needed to match any updates in requirements and application.

--

--

Maria Golubeva
JAGAAD Digital Solutions

Belarusian | QA Engineer in Italian company | Mentor in Tech | Web Automation | Improving English and Italian levels | Travel lover