A Simple and Effective E2E Test Architecture with Playwright and TypeScript

A Simple and Effective E2E Test Architecture with Playwright and TypeScript

TL;DR

In this article, we discussed an effective structure for E2E testing using TypeScript and Playwright. Key points:

- Clear separation of components into API and UI for easier navigation and maintenance.

- Application of the "Feature Object" approach, which replaces the traditional "Page Object" and focuses on user-oriented features.

- Use of the Arrange-Act-Assert pattern for structuring tests, making them more readable and maintainable.

- Selection of reliable selectors (getByRole, getByText, getByTestId) for stable tests.

- Examples of using Page Object for centralized management of actions and locators, improving code organization and maintainability.


Introduction

Hi! My name is Denis, and I specialize in test automation. Throughout my career, I've worked on various projects and seen many approaches to organizing tests and the Page Object Model (POM). I often encountered shortcomings in existing structures: some were complex, while others were simple but inefficient. This inspired me, together with my team, to develop a new approach that would be effective and straightforward. In our project, we use TypeScript and Playwright to create reliable and maintainable tests.

Problems with Existing Structures

Many developers face issues when organizing their test projects. Here is a common structure:

At first glance, this structure seems logical, but it has several drawbacks:

1. Mixing tests and Page Objects: Tests and Page Objects are in the same directory, making navigation and code maintenance difficult. Developers have to search for the necessary files among many others, slowing down the development process.

2. Lack of clear separation between API and UI: Methods for interacting with API and UI are mixed, complicating testing and understanding of the structure. Changes in one component can unintentionally affect another, leading to errors and increasing debugging time.

3. Complicated maintenance and scalability: Adding new tests and methods can cause confusion, complicating the maintenance and scaling of the project. This increases the likelihood of errors and decreases the quality of tests.

As a result, such a structure complicates navigation, makes test maintenance and scalability more difficult, and increases the likelihood of errors.

Solution: New Project Structure

We developed a structure that addresses these problems by clearly separating components. Here's what it looks like:

Advantages of the Proposed Structure

1. Clear separation of components: Separating tests, Page Objects, API, and UI methods makes the structure more understandable and navigation easier. Each file is responsible for a specific functionality aspect, making the code more maintainable.

2. Readable methods: Using clear and readable method names that explicitly relate to a specific feature improves code readability and simplifies its usage.

3. Simplified maintenance: It is easier to maintain and update tests and Page Objects since they are in separate directories. This allows developers to find and fix errors faster.

4. Improved scalability: Adding new tests and methods becomes simpler and doesn't cause confusion. The structure allows the project to scale easily without significant changes to the existing code.

In conclusion, the proposed structure solves the problems of existing approaches, simplifying navigation and maintenance, and improving the scalability and reliability of tests.

Feature Object

This principle implies that each Feature Object corresponds to a specific feature. It simplifies understanding what each file is responsible for and reduces the risk of accidental changes in other parts of the application.


Why We Use Feature Object

In front-end terms, Page and Feature are very close terms as they almost always represent the same thing. For example, the project editing feature and the project editing page.

We use the Feature Object because the front-end, through specific views (e.g., pages), solves specific user problems (features). If the interface changes from a page to a text chat or a voice chat, the feature itself doesn't change.

Technically, when writing tests for the front-end, there isn't much difference, but we build the model to emphasize testing features for real users, not just front-end pages.

Why It Works Better:

1. Clarity and code readability: Each Feature Object is responsible for a specific feature, simplifying project navigation and allowing developers to quickly find the necessary files and methods. Separating Feature Objects by features simplifies project navigation, speeding up development and debugging.

2. Ease of code modification: Changes in one feature affect only one Feature Object. This reduces the risk of unintended changes in other parts of the application and decreases code duplication. By calling methods from other Feature Objects, we adhere to the DRY (Don't Repeat Yourself) principle, making the code more maintainable.

3. Improved testability: Localizing changes in one Feature Object simplifies writing and maintaining E2E tests for specific features. The scalability of the structure allows adding new features without significant changes to the existing code base, making the project flexible and adaptable to new requirements.

Separation of API and UI

Storing API and UI methods separately allows for clear delineation of responsibility and reduces dependency between components, simplifying code testing and maintenance.

Fixtures

Fixtures make it easy to integrate API and UI methods in tests, making them accessible and simplifying switching between different contexts. Here is an example of such a structure:

The clear separation of responsibilities between the API and UI levels, along with the use of fixtures to encapsulate the initialization of these levels, makes it easy to set up testing contexts and reuse code. This significantly simplifies the maintenance and expansion of tests.

Base Classes for API and UI

Using base classes for API and UI simplifies the creation and maintenance of tests.

Examples of Feature Objects

In this section, I want to show you a few examples of Feature Objects to demonstrate how you can organize your code to improve readability and maintainability. We follow the "Feature Object" approach, which allows for easy management and maintenance of the code. In the examples below, you will see how a clear separation of responsibilities helps improve project structure and makes working with tests easier.

ordersUi.ts

Imagine you have an order management system. All actions related to orders should be centralized in one place to easily make changes and maintain the code. In the OrdersUi class, we have gathered all the methods and locators related to working with orders.

With this code, if you need to open an order or verify its completion, you have all the necessary methods in one class. This makes the code more organized and easier to maintain. The radical change is that instead of distributing methods across different parts of the project, they are collected in one class, simplifying navigation and management.

paymentsUi.ts

Now let's look at an example of managing payments. In the PaymentsUi class, we have centralized all actions related to payments, allowing us to easily manage payment processes.

With this code, you can easily add new payment methods and manage them from a centralized class. This greatly simplifies the testing and maintenance of payment-related features. The advantage here is that all interactions with payments are gathered in one place, avoiding code duplication and making it easier to make changes.

Example Test Using Feature Object

Now let's see how you can use these Feature Objects in your tests. By applying the principles of responsibility separation and using fixtures, we can make our test code cleaner and more maintainable.

These examples show how clear responsibility separation and the use of Feature Objects can simplify writing and maintaining tests. You can easily manage and update tests knowing that all related methods and locators are in one place.

Principles of Choosing Selectors

To write stable tests, it is important to choose the right element selectors. The main criteria for selection:

1. getByRole: Use if the element has a role. This helps improve accessibility and test reliability.

2. getByText: Use if the element has no role but has text. This is useful for elements with visible labels, improving accessibility.

3. getByTestId: If the element has no role or text, use data-testid. This method allows you to accurately identify elements. It is important to agree with the development team on adding these identifiers and ensure they are unique. Using data-testid improves test accuracy as changes in layout have less impact on tests.

4. Classes and XPath: Use only in extreme cases and for debugging. It is recommended to avoid using such selectors in main tests as they are less stable and more prone to changes in the code.

Example of Using Selectors

When Classes and XPath Are Acceptable:

1. Temporary solution for debugging: Use classes and XPath only temporarily for debugging, do not merge PRs with such selectors.

2. Lack of other localization methods: If there is no way to use getByRole, getByText, or getByTestId, then using classes or XPath may be justified. However, it is recommended to review the code architecture and find more stable ways to localize elements.

3. Complex data structures: In some cases, complex data structures may require using XPath, for example, to select elements within complex hierarchies, but this should be the exception, not the rule.

Arrange-Act-Assert Pattern

In our approach, we adhere to the Arrange-Act-Assert pattern, which means preparation (Arrange), performing actions (Act), and checking results (Assert). This pattern helps structure tests and makes them more understandable and maintainable.

In all three steps, both UI and API can be used for interaction and verification. This depends on the specific scenario and what is more convenient or effective in each particular case.

1. Arrange (Preparation): In this step, we prepare all the necessary data and state for the test. For example, we can use the API to create data needed for the test.

2. Act (Execution): Here we perform the actions that our scenario tests. These can be interactions with the UI or API calls.

3. Assert (Verification): At this stage, we check the results of the actions. We can use both UI and API to verify the correctness of the result.

Example Test Using Arrange-Act-Assert Pattern

Note that in these examples, the Arrange-Act-Assert pattern helps structure tests, making them more logical and maintainable. Using both UI and API methods allows choosing the most effective tools for each specific step.

Conclusion

I hope this article has helped you understand how to structure a project for E2E testing and create Feature Objects for convenient and effective testing. We have discussed important aspects of creating an effective structure for E2E testing using TypeScript and Playwright. Considering the principles of the Page Object Model, choosing reliable locators, and separating responsibilities between API and UI methods helps create tests that are easy to maintain and expand.

The main advantages of the proposed approach include the clear separation of components, the use of understandable and readable method names, and the Feature Object approach. These elements simplify code navigation and maintenance, reduce the risk of accidental changes, and improve project scalability. Applying the Arrange-Act-Assert pattern structures tests, making them more organized and logical.

Using the proposed architecture allows you to set up and maintain automated tests faster, improve code readability and scalability, and reduce risks and increase the reliability of tests. I hope this approach will be useful to you and help improve your test automation practices.

Petr Glaser 🤖

Code smarter with AI: Boost your team's efficiency | Transform your coding approach | Experience brilliance today 🔥

1mo

I have to check and also recommend it to Lýdie Hemalová🐞. :)

Naeem Malik

QC Engineer @ Netwrix Corporation | Selenium Specialist, Azure DevOps, C#/Java

1mo

Very nice work, thanks for sharing.

Adrian Maciuc

Software Quality Assurance Automation Engineer | Python | Javascript | Java | Cypress | Playwright | Selenium

1mo

This is a very good structure. A few points if I may ask: 1. Title says Playwright with javascript but you are using typescript. Typo? 2. See screenshot. What is test.ts used for? 3. I was happy to see separation between API and UI. But then I see class BaseUi with this.api in it. So is it separate or API is in BaseUi ? Maybe the name BaseUi should be more generic to include both UI and API ? 4. I see "Example Test Using Feature Object" . And in that test I see import of OrdersUi, PaymentsUi and others from e2e-shared. Why the imports? 5. I see "Example of Using Selectors". But where do you put the selectors in the structure? Under what file/folder? This is key information that its missing I love the structure and I am happy people give more love to ideas such as Feature Object, and replace the old POM.

  • No alternative text description for this image
Kailash Pathak

└► Linkedin Top Quality Assurance Voice | Applitools Ambassador | Cypress Ambassador | Read My Blog at qaautomationlabs.com | 2x AWS,PMI-ACP®,ITIL® PRINCE2 Practitioner®

1mo

Insightful!

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics