Focus on testing the behavior and user interactions of your application. Write tests that simulate real user actions and validate the expected outcomes. This approach helps ensure that your tests cover the intended functionality.
Each test should be independent of others, meaning that the state or outcome of one test should not affect another. Isolated tests are easier to debug, maintain, and scale.
Third-party services or APIs should be mocked or stubbed out. This avoids flaky tests due to external service outages and ensures that your tests are focused on your application code.
When using locators in Playwright, prefer them in the following order from most preferred to least preferred:
getByTestId(Most preferred)getByTitlegetByRolegetByLabelgetByPlaceholdergetByAltTextgetByTextlocator(Avoid if possible)
Prefer web-first assertions provided by Playwright, which automatically wait for elements to be ready before asserting. This prevents flakiness due to timing issues.
-
Correct:
await expect(page.getByText('success')).toBeVisible();
-
Incorrect:
expect(await page.getByText('success').isVisible()).toBe(true);
Ensure that each test contains at least one assertion. A test without an assertion is just a series of actions, which defeats the purpose of testing.
-
Incorrect:
test('test without assertion', async ({ page }) => { await page.getByTestId('add-task-button').click(); });
-
Correct:
test('test with assertion', async ({ page }) => { await page.getByTestId('add-task-button').click(); await expect(page.getByTestId('task-item')).toHaveCount(1); });
Deeply nested blocks can make tests harder to read and maintain. Keep nesting shallow and meaningful.
Ensure that all asynchronous actions are awaited to prevent unexpected behaviors in tests.
Tests should be deterministic. Avoid using conditional statements (if, switch, etc.) within test blocks as they can lead to unpredictable test outcomes.
Prefer using locators over element handles. Locators offer more powerful and flexible operations.
-
Incorrect:
const buttonHandle = await page.$('button'); await buttonHandle.click();
-
Correct:
const buttonLocator = page.locator('button'); await buttonLocator.click();
Forcing actions like clicks can hide underlying issues in your tests. Avoid using { force: true } unless absolutely necessary, and always investigate the cause of the issue first.
Relying on networkidle for waiting can be unreliable, especially with modern SPAs that often have ongoing network requests. Prefer more specific wait conditions.
Using .first(), .last(), and .nth() locators can make tests fragile. It's better to be more explicit with your locators.
- Avoid:
page.locator('button').first(); page.locator('button').last(); page.locator('button').nth(3);
Be careful when using negative assertions, as they might not always be the best approach.
-
Incorrect:
await expect(locator).not.toBeVisible();
-
Correct:
await expect(locator).toBeHidden();
For counting assertions:
-
Incorrect:
await expect(locator).not.toHaveCount(1);
-
Correct:
await expect(locator).toHaveCount(0);