Understanding Fixtures in Playwright and Why You Should Use Them
Playwright is an amazing framework that makes testing applications a breeze for testers and developers. In this article, we’ll dive into the world of Playwright fixtures — these handy features not only make testing easier but also offer reasons why you should jump on board and start using them in your testing adventures.
What are Fixtures in Playwright?
Fixtures in tests are not a new thing introduced by Playwright.
In the context of software a test fixture (also called “test context”) is used to set up system state and input data needed for test execution. For example, the Ruby on Rails web framework uses YAML to initialize a database with known parameters before running a test. This allows for tests to be repeatable, which is one of the key features of an effective test framework.
Fixtures in Playwright serve as reusable setups for tests, providing a consistent environment for performing actions against web applications. They encapsulate shared setup and teardown logic, ensuring a uniform starting point for tests. Essentially, fixtures help maintain code readability, reduce redundancy, and enhance the scalability of test suites.
Why Use Fixtures?
- Code Reusability: Fixtures enable the creation of reusable code blocks, promoting modular test development and minimizing duplication of setup steps across multiple tests.
- Consistency: With fixtures, tests begin in a consistent state, eliminating variations caused by disparate setups and ensuring reliable test outcomes.
- Maintainability: Centralizing setup and teardown operations within fixtures simplifies maintenance, making it easier to update or modify shared functionalities.
This is an example code when a developer is not utilizing fixtures in playwright,
import { test, expect } from '@playwright/test';
import { LoginPage } from 'tests/pages/login-page';
import { LandingPage } from 'tests/pages/landing-page';
test('Test 1: Login and Perform Action on Landing Page', async ({ page, baseURL, request }) => {
// Setup - Login Page
const loginPage = new LoginPage(page, baseURL, request);
await loginPage.removeAll();
// Test Actions
await loginPage.navigateToLoginPage();
await loginPage.login('username', 'password');
// Setup - Landing Page
const landingPage = new LandingPage(page);
// Perform actions on Landing Page after successful login
await landingPage.doSomething();
// Assertions
await expect(landingPage.getSomething()).toBeTruthy();
// Teardown - Logout or Reset State
// Example: await loginPage.logout();
});
test('Test 2: Another Login and Similar Action on Landing Page', async ({ page, baseURL, request }) => {
// Setup - Login Page (Duplicated setup logic)
const loginPage = new LoginPage(page, baseURL, request);
await loginPage.removeAll();
// Test Actions (Similar actions as Test 1)
await loginPage.navigateToLoginPage();
await loginPage.login('differentUsername', 'differentPassword');
// Setup - Landing Page (Duplicated setup logic)
const landingPage = new LandingPage(page);
// Perform actions on Landing Page after successful login
await landingPage.doSomething();
// Assertions (Similar assertions as Test 1)
await expect(landingPage.getSomething()).toBeTruthy();
// Teardown - Logout or Reset State (Duplicated teardown logic)
// Example: await loginPage.logout();
});Below is an example of how we can reduce the code duplication using Playwright fixtures.
How to Use Fixtures in Playwright:
- Define fixtures using the
test.extendmethod, specifying setup, and teardown logic.
// fixture.js
import { test as base } from '@playwright/test';
import { LoginPage } from 'tests/pages/login-page';
import { LandingPage } from 'tests/pages/landing-page';
type TestFixtures = {
loginPage: LoginPage;
landingPage: LandingPage;
};
export const test = base.extend < TestFixtures >( {
landingPage: async ( { page }, use ) => {
const landingPage = new LandingPage( page );
await use( landingPage );
},
loginPage: async ( { page, baseURL, request }, use ) => {
const loginPage = new LoginPage( page, baseURL, request );
await use( loginPage );
await loginPage.removeAll();
},
} );2. Incorporate fixtures into tests by referencing them in the test definition.
// sampletest.spec.js
import { test } from './fixture'; // using the test extended previously.
import { expect } from '@playwright/test';
test('Test 1: Login and Perform Action on Landing Page', async ({ loginPage, landingPage }) => {
// Test Actions
await loginPage.login('username', 'password');
// Perform actions on Landing Page after successful login
await landingPage.doSomething();
// Assertions
await expect(landingPage.getSomething()).toBeTruthy();
});
test('Test 2: Another Login and Similar Action on Landing Page', async ({ loginPage, landingPage }) => {
// Test Actions (Similar actions as Test 1)
await loginPage.login('differentUsername', 'differentPassword');
// Perform actions on Landing Page after successful login
await landingPage.doSomething();
// Assertions (Similar assertions as Test 1)
await expect(landingPage.getSomething()).toBeTruthy();
});Real-world use cases of Fixtures:
1. Authentication Fixtures:
Many web applications require authentication to access certain functionalities. A fixture can be created to handle login/logout actions, ensuring that tests requiring authenticated access can reuse this fixture. This simplifies the process for testing functionalities available only after login.
2. Data Setup and Teardown:
Testing scenarios that involve creating, modifying, or deleting data (e.g., user profiles, products) often require a specific initial state. Fixtures can be used to set up this data before tests and clean it up afterward, ensuring that each test starts with a consistent data state and doesn’t leave behind unwanted artifacts.
3. Environment Configuration:
Tests might need different environments (e.g., staging, production) or configurations (e.g., different browsers, devices) to be validated thoroughly. Fixtures can be created to set up these varied environments, allowing tests to run seamlessly across different configurations without duplicating setup logic.
4. Navigation and Page Setup:
Some tests require navigating to specific pages or performing certain actions before actual testing begins. Fixtures can handle these navigation tasks, ensuring tests start from a known state or perform certain initial interactions (e.g., filling out a form, clicking through a series of pages).
5. Error Handling and Recovery:
Tests often encounter unexpected errors or exceptions. Fixtures can be utilized to set up error-handling mechanisms or recovery actions, such as resetting the state or refreshing the page, enabling tests to continue or gracefully handle failures.
6. Mocking External Dependencies:
When testing functionalities interacting with external services or APIs, fixtures can facilitate the setup of mocks or stubs for these dependencies. This ensures that tests are not affected by fluctuations or unavailability of external services, allowing for controlled and reliable testing.
Best Practices for Fixtures:
- Keep Fixtures Atomic: Maintain fixtures’ independence from one another to ensure they can be used interchangeably across tests.
- Reuse Fixtures Wisely: Identify common setup/teardown logic to encapsulate within fixtures, enhancing test suite maintainability.
- Strive for Clarity: Name fixtures descriptively, highlighting their purpose and making test code more readable.
- Regular Maintenance: Update fixtures as the application evolves to align with changes in the tested application’s behavior.
In conclusion, fixtures in Playwright play a crucial role in structuring tests, promoting reusability, and ensuring consistency across test suites. By leveraging fixtures effectively, developers and QA engineers can streamline the testing process, enhance code maintainability, and create more robust automation scripts.






