Block Unnecessary Requests
In many cases, your tests don't require all the network requests a real user might make. Blocking unnecessary requests like third-party analytics can significantly speed up your tests. Here's how you can do it:
await page.route('**/analytics/**', route => route.abort());
Reuse Test Code with Functions
Instead of repeating similar code in multiple tests, you can define reusable functions and call them in your test scripts. This not only makes your tests cleaner and more organized but also easier to maintain.
async function login(page) {
await page.goto('https://ray.run/login');
await page.getByLabel('Username or email address').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
}
test('first', async ({ page }) => {
await login(page);
// Your test code here
});
test('second', async ({ page }) => {
await login(page);
// Your test code here
});
Leverage APIs to Shortcut Lengthy Workflows
In end-to-end testing, it's not uncommon to come across lengthy workflows that involve a lot of steps, such as user registration or login. Instead of simulating these workflows through the UI, which can be slow and increase the chances of flaky tests, consider leveraging your application's APIs to shortcut these processes.
By interacting directly with your API, you can setup preconditions and clean up data more efficiently. For example, instead of going through the entire login process via the UI, you can make an API call to authenticate a user and start the test with an authenticated session.
async function authenticateUser() {
const response = await fetch('https://ray.ryn/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'test', password: 'test' }),
});
const { sessionToken } = await response.json();
return sessionToken;
}
test('authenticated test', async ({ context }) => {
const sessionToken = await authenticateUser();
// Store the session token as a cookie or in local storage.
await context.addCookies([
{ name: 'sessionToken', value: sessionToken, domain: 'ray.ryn', path: '/', httpOnly: true },
]);
// Now you can visit a page as an authenticated user.
await page.goto('https://ray.ryn/dashboard');
});
Keep in mind that while this strategy can speed up your tests, it doesn't replace the need to test these workflows through the UI. You should still write end-to-end tests that go through the login and registration process as a real user would.
Leverage Headless Mode for Faster Execution
In headless mode, the browser runs in the background without displaying a GUI. This often leads to faster execution times, making your testing more efficient. While you might want to occasionally run your tests with the browser UI for debugging purposes, consider using headless mode for regular testing.
const browser = await playwright.chromium.launch({ headless: true });
Write tests as if they will be running in parallel
While it's not always practical or necessary to run every test in parallel, it's a good practice to write your tests as if they will be. Doing so will help you avoid common pitfalls that can make your tests flaky and hard to maintain.
Here are some tips for writing tests with parallel execution in mind:
Maintain Test Independence
Each test should be completely independent from one another. This means a test should not rely on the result or side effect of another test. By writing tests this way, you can run them in any order or in parallel without worrying about unexpected dependencies.
Isolate Test Data
When writing tests that interact with data, it's crucial to isolate the data used by each test. Avoid using shared or global data that could be modified by multiple tests running concurrently. Consider using tools or techniques that provide unique, isolated test data for each test. For example, if you're testing a database-driven application, you might want to use a separate database or schema for each test or test run.
Use Unique Identifiers
When creating resources (like users or records in a database) as part of your tests, use unique identifiers. This ensures that even if tests are running concurrently, they won't conflict with each other.
Handle Network Throttling and Rate Limiting
If your tests involve making requests to third-party APIs, be aware of rate limiting and network throttling. Running many tests in parallel could quickly exhaust API rate limits, causing your tests to fail.
Prioritize User-Facing Attributes
Use Locators with Auto-Waiting
When writing end-to-end tests, you need to find elements on the web page. Playwright's built-in locators come with auto-waiting and retry-ability, ensuring the element is visible and enabled before performing actions. To make tests resilient, prioritize user-facing attributes and explicit contracts.
// Good practice
await page.getByRole('button', { name: 'submit' });
Chain and Filter Locators
You can chain locators to narrow down the search to a specific part of the page. Additionally, you can filter locators by text or another locator.
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
Avoid DOM-Dependent Locators
Your DOM can easily change, and having your tests depend on the DOM structure can lead to failing tests. Avoid selecting elements by their CSS classes or other attributes that are likely to change during design updates. Instead, use locators that are resilient to changes in the DOM.
// Bad practice
await page.locator('button.buttonIcon.episode-actions-later');
// Good practice
await page.getByRole('button', { name: 'submit' });
Leverage Playwright's Test Generator
Playwright has a test generator that can generate tests and pick locators for you. It prioritizes role, text, and test ID locators and automatically improves the locator to uniquely identify target elements.
To pick a locator, run the codegen
command followed by the URL you want to pick a locator from:
npx playwright codegen ray.run
You can also use the VS Code Extension to generate locators and record a test, further improving your developer experience.
Use Web-First Assertions
Playwright provides web-first assertions that wait until the expected condition is met. This eliminates the need for manual waiting and makes your tests more reliable.
// Good practice
await expect(page.getByText('welcome')).toBeVisible();
// Bad practice
expect(await page.getByText('welcome').isVisible()).toBe(true);
Keep Playwright Up to Date
Keeping your Playwright version up to date allows you to test your app on the latest browser versions and catch failures before they affect users. To update Playwright, run:
npm install -D @playwright/test
Check the release notes for the latest changes and updates.
Integrate Playwright Tests with CI/CD
Set up CI/CD and run your tests frequently. Ideally, you should run your tests on each commit and pull request. Playwright comes with a GitHub Actions workflow that runs tests on CI for you with no setup required. You can also set up Playwright on the CI environment of your choice.
Utilize Playwright's Tooling
Playwright offers a range of tooling to help you write tests:
- The VS Code extension improves your developer experience when writing, running, and debugging tests.
- The test generator generates tests and picks locators for you.
- The trace viewer provides a full trace of your tests as a local PWA that can easily be shared.
- TypeScript support works out of the box and improves IDE integrations.
By implementing these tips and best practices, you'll be on your way to writing more efficient and maintainable Playwright test scripts. Remember, testing is an essential part of any development process, and with the right tools and techniques, you can streamline your testing workflow and ensure that your web applications are reliable and bug-free. Happy testing!