Modular Testing with Playwright and Xray

Modular Testing with Playwright and Xray


The modular pattern of software testing includes both writing and organizing tests. In the modular test, tests are written as small, function-based tests and then placed in execution within the test suite. Each function-specific test provides particular user behavior.

With the Modular testing Approach, we can save time by reusing the already created Module of the test and save the time of QA to rewriting the same scenarios. Also, change in one place affects on followed test cases also.

Modular Testing in X-Ray

Writing the same test cases for each module is always a chore. It is not just a waste of time, but also a waste of effort. Test cases should always be reusable. The Modular test design technique allows us to reuse test cases across many projects and modules. Modular test design is commonly used for End-to-end testing. For example, suppose you have an e-commerce application and want to test the Add to Cart feature. The user must be logged in to the application to add a product to the cart, You've already written the Login test cases for the application, so you don't need to repeat them in the Adding product to cart test cases. You can just call the steps of Login test cases in the test case of Adding a product to the cart and it is possible only with the X-Ray test case management tool.

Advantages of reusing test cases

Instead of updating several test cases across multiple projects and modules, you only need to update the test cases when a feature changes. As a result, maintenance costs are reduced. Test cases are written for specific features rather than the entire project, allowing for more attention to each feature and more robust tests.

For more info related to modular tests, refer to this doc: Modular Tests - Xray Cloud Documentation - Xray

Modular Testing With Playwright

A playwright is a free open-source framework for automation tests. In Playwright we can create Modular tests using page object model patterns and test fixtures.

Page Object Model

Page Object Model is a common pattern that introduces abstractions over web app pages to simplify interactions with them in multiple tests.

We will create a helper class to encapsulate common operations of the page. Internally, it will use the object.

// playwright-dev-page.ts
import { expect, Locator, Page } from '@playwright/test';
export class PlaywrightDevPage {
read-only page: Page;
readonly getStartedLink: Locator;
readonly gettingStartedHeader: Locator;
readonly pomLink: Locator;
readonly tocList: Locator;
constructor(page: Page) { = page;
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
this.gettingStartedHeader = page.locator('h1', { hasText: 'Getting started' });
this.pomLink = page.locator('li', { hasText: 'Playwright Test' }).locator('a', { hasText: 'Page Object Model' });
this.tocList = page.locator('article div.markdown ul > li > a');
async goto() {
async getStarted() {
await this.getStartedLink.first().click();
await expect(this.gettingStartedHeader).toBeVisible();
async pageObjectModel() {
await this.getStarted();

Now we can use the class in our tests.

// example.spec.ts
import { test, expect } from '@playwright/test';
import { PlaywrightDevPage } from './playwright-dev-page';

test('getting started should contain table of contents, async ({ page }) => {
    const playwrightDev = new PlaywrightDevPage(page);
    await playwrightDev.goto();
    await playwrightDev.getStarted();
    await expect(playwrightDev.tocList).toHaveText([
    'First test',
    'Configuration file',
    'Writing assertions',
    'Using test fixtures',
    'Using test hooks',
    'VS Code extension',
    'Command line',
    'Configure NPM scripts',
    'Release notes'

test('should show Page Object Model article', async ({ page }) => {
    const playwrightDev = new PlaywrightDevPage(page);
    await playwrightDev.goto();
    await playwrightDev.pageObjectModel();
    await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');


Playwright Test is based on the concept of test fixtures. Test fixtures are used to establish an environment for each test, giving the test everything it needs and nothing else. Test fixtures are isolated between tests. With fixtures, you can group tests based on their meaning, instead of their common setup.

Here is a list of the pre-defined fixtures that you are likely to use most of the time:

Fixture Type Description
Page Page Isolated page for this test run.
context BrowserContext Isolated context for this test run. The fixture belongs to this context as well. Learn how to configure context.
browser Browser Browsers are shared across tests to optimize resources. Learn howthe to configure browser.
browserName string The name of the browser currently running the tesomniumther or .

With fixtures

Fixtures have several advantages over before/after hooks:

  • Fixtures encapsulate setup and teardown in the same place so it is easier to write.
  • Fixtures are reusable between test files - you can define them once and use them in all your tests. That's how Playwright's built-in fixture works.
  • Fixtures are on-demand - you can define as many fixtures as you'd like, and the Playwright Test will set up only the ones needed by your test and nothing else.
  • Fixtures are composable - they can depend on each other to provide complex behaviors.
  • Fixtures are flexible. Tests can use any combinations of the fixtures to tailor the precise environment they need, without affecting other tests.
  • Fixtures simplify grouping. You no longer need to wrap tests in that set up the environment, and are free to group your tests by their meaning instead.
// example.spec.ts
 import { test as base } from '@playwright/test';
 // Page object model Class File
 import { TodoPage } from './todo-page'; 
 // Extend basic test by providing a "todoPage" fixture.
 const test = base.extend <{ todoPage: TodoPage } >({
     todoPage: async ({ page }, use) => {
     const todoPage = new TodoPage(page);
     await todoPage.goto();
     await todoPage.addToDo('item1');
     await todoPage.addToDo('item2');
     await use(todoPage);
     await todoPage.removeAll();
 test('should add an item', async ({ todoPage }) => {
     await todoPage.addToDo('my item');
     // ...
 test('should remove an item', async ({ todoPage }) => {
     await todoPage.remove('item1');
     // ...

Creating a fixture

To create your own fixture, use test.extend(fixtures) to create a new object that will include it.

Below we create two fixtures and that follow the Page Object Model pattern.

/ my-test.ts
 import { test as base } from '@playwright/test';
 import { TodoPage } from './todo-page';
 import { SettingsPage } from './settings-page';
 // Declare the types of your fixtures.
 type MyFixtures = {
     todoPage: TodoPage;
     settingsPage: SettingsPage;
 // Extend base test by providing "todoPage" and "settingsPage".
 // This new "test" can be used in multiple test files, and each of them will get the fixtures.
 export const test = base.extend <MyFixtures>({
     todoPage: async ({ page }, use) => {
     // Set up the fixture.
     const todoPage = new TodoPage(page);
     await todoPage.goto();
     await todoPage.addToDo('item1');
     await todoPage.addToDo('item2');
     // Use the fixture value in the test.
     await use(todoPage);
     // Clean up the fixture.
     await todoPage.removeAll();
     settingsPage: async ({ page }, use) => {
     await use(new SettingsPage(page));
 export { expect } from '@playwright/test';

Using a fixture

Just mention fixture in your test function argument, and test runner will take care of it. Fixtures are also available in hooks and other fixtures. If you use TypeScript, fixtures will have the right type.

Below we use the and fixtures defined above.

import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.locator('.todo-item')).toContainText(['something nice']);

Overriding fixtures

In addition to creating your own fixtures, you can also override existing fixtures to fit your needs. Consider the following example which overrides the fixture by automatically navigating to some :

import { test as base } from '@playwright/test';                        
 export const test = base.extend({
     page: async ({ baseURL, page }, use) => {
     await page.goto(baseURL);
     await use(page);