Logo
  • Home
  • Services
  • Blog
  • Company
Contact Us

Playwright: Efficient Verification with Soft Assertions

Home > Blog > Playwright: Efficient Verification with Soft Assertions

Author: Tomotaka ASAGI

Published: Jan 31, 2026

image

Introduction

After years of working with E2E automation, you inevitably encounter all sorts of testing scenarios. The most frequent request is usually the same: "Automate these manual tests."

In the early days, I used to automate these tests without much forethought, and things often went sideways. For instance, a single manual test case often intertwines operations and checkpoints, requiring dozens of validations. Whether you view this as one large test or multiple smaller ones, the automation ends up as a repetitive sequence of "step → check → step → check..."

This raises a critical question: When a single check fails, should we skip the rest? Should the entire test case be marked as failed immediately?

When testing manually, you can visually inspect a screen and judge multiple items at once—"this passes, that fails." Automated testing, however, doesn't inherently work that way; usually, the script stops the moment it hits an error.

Nowadays, we'd typically handle fine-grained checks in unit tests following the "one test, one assertion" principle. But applying that same logic to E2E tests is a different story. If you have 100 checkpoints on a single screen, you don't want to log in and navigate there 100 separate times just to follow a principle. That's a massive waste of time and resources.

This is where you need a framework that supports "soft assertions"—the ability to continue testing even after a validation fails. I used to build these custom functions myself, but Playwright now has this capability built-in.

Soft Assertions: Verification That Continues After Failure

The Difference Between Hard and Soft Assertions

Hard Assertion (regular expect)

await expect(page.getByTestId('name')).toHaveText('John Smith');
// ↑ Test stops immediately on failure
await expect(page.getByTestId('email')).toHaveText('john@example.com');
// ↑ This won't run if the above fails

Soft Assertion (expect.soft)

await expect.soft(page.getByTestId('name')).toHaveText('John Smith');
// ↑ Test continues even on failure
await expect.soft(page.getByTestId('email')).toHaveText('john@example.com');
// ↑ This runs even if the above fails

Benefits of Soft Assertions

When verifying multiple items on a read-only screen, Soft Assertions let you identify all issues in a single test run.

With traditional Hard Assertions:

  • First one fails → fix → re-run
  • Second one fails → fix → re-run
  • ...repeat

With Soft Assertions:

  • One run shows all 6 results
  • Fix everything at once → re-run

Guidelines for Choosing

Scenario
Recommendation
Login success verification
Hard Assertion
Page navigation verification
Hard Assertion
Multiple field verification on read-only screens
Soft Assertions
Form validation verification
Soft Assertions
Tests where previous steps must succeed
Hard Assertion

Early Exit with Checkpoint

If you want to use Soft Assertions but stop at certain points when errors occur:

Using expect.configure for Default Soft Behavior

When writing many Soft Assertions, expect.configure makes your code cleaner.

Adding Custom Messages

You can add custom messages to make failure reports clearer.

await expect.soft(
  page.getByTestId('customer-name'),
  'Customer name should be displayed correctly'
).toHaveText('John Smith');

Important Constraint

Soft assertions only work with Playwright test runner.

This is one of the "TypeScript-only features" I mentioned in Part 2. It's not available in the Java version of Playwright.

Using Soft Assertions with Playwright-bdd

Designing Then Steps with Soft Assertions

In BDD, traditionally one Then step represents one verification. However, with Soft Assertions, you have the option to combine multiple related verifications into a single Then step.

Option 1: Traditional 1 Then = 1 Assertion

Scenario: Display customer details
  Given the user is logged in
  And the customer detail screen is open
  Then the customer name "John Smith" is displayed
  And the customer email "john@example.com" is displayed
  And the customer phone "555-1234" is displayed

Option 2: Combine with Soft Assertions

Scenario: Display customer details
  Given the user is logged in
  And the customer detail screen is open
  Then the customer basic information is displayed correctly
Then('the customer basic information is displayed correctly', async ({ page }) => {
  await expect.soft(page.getByTestId('customer-name')).toHaveText('John Smith');
  await expect.soft(page.getByTestId('customer-email')).toHaveText('john@example.com');
  await expect.soft(page.getByTestId('customer-phone')).toHaveText('555-1234');
});

Option 3: Use Scenario Outline with Examples

When you have many verification items or need to repeat similar verifications, Scenario Outline with Examples is also effective.

Scenario Outline vs Soft Assertions

Aspect
Scenario Outline + Examples
Soft Assertions
Test case handling
Each row is an independent test case
Multiple verifications in one test case
Behavior on failure
Other rows still run if one fails
Other verifications still run if one fails
Report display
Pass/Fail shown per row
Multiple failures shown in one test
Feature file
Target and expected values clearly visible
Verification details abstracted

Which to Choose

Option
Pros
Cons
1 Then = 1 Assertion
Feature file is detailed and readable
Many steps, stops on first failure
Combined Soft Assertions
Efficient, see all issues at once
Feature file becomes abstract
Scenario Outline + Examples
Data in table format, rows are independent
Requires mapping logic in step definitions

My approach for read-only screen verification:

  • Few items (3-5): Combine with Soft Assertions
  • Many items, or want table-based management: Scenario Outline + Examples
  • Business logic verification: Individual Then steps

Revisiting "One Test, One Assertion"

The "One Test, One Assertion" approach I mentioned at the beginning made sense in a world with only Hard Assertions.

However, with Soft Assertions, it's better to think of this as having more options.

Approach
When to Apply
One Test, One Assertion
State transition tests, critical flows
One Test, Multiple Assertions (Soft)
Read-only screens, form validation

It's not about which is "correct" — it's about choosing based on the situation.

Summary

Feature
Previous Challenge
Playwright Solution
Soft Assertions
Everything stops on first failure, required custom implementation
Provided as standard feature

By combining storageState + Projects from the previous article with Soft Assertions from this article, you can write efficient tests for authenticated read-only screens.

References

  • Playwright Test Assertions
  • Playwright-bdd Documentation

This article is Part 5 of the "Playwright Series."

  • Part 1: From Selenium to Playwright
  • Part 2: TypeScript vs Java - Feature Differences by Language
  • Part 3: BDD Framework Comparison - Cucumber.js vs Playwright-bdd
  • Part 4: Streamlining Authentication with storageState
  • Part 5: Efficient Verification with Soft Assertions (this article)

Home

About Us

Services

Blog

Contact Us

Privacy Policy

Cookie

©ARRANGILITY SDN. BHD.

test('Customer detail screen - verify all fields', async ({ page }) => {
  await page.goto('/customer/123');

  // Verify all fields with Soft Assertions
  await expect.soft(page.getByTestId('customer-name')).toHaveText('John Smith');
  await expect.soft(page.getByTestId('customer-email')).toHaveText('john@example.com');
  await expect.soft(page.getByTestId('customer-phone')).toHaveText('555-1234');
  await expect.soft(page.getByTestId('customer-address')).toBeVisible();
  await expect.soft(page.getByTestId('order-count')).toHaveText('5 orders');
  await expect.soft(page.getByTestId('last-order-date')).toHaveText('2025/01/15');
});
test('Customer detail screen', async ({ page }) => {
  await page.goto('/customer/123');

  // Verify basic info with Soft Assertions
  await expect.soft(page.getByTestId('customer-name')).toHaveText('John Smith');
  await expect.soft(page.getByTestId('customer-email')).toHaveText('john@example.com');

  // Check if any soft assertions have failed so far
  expect(test.info().errors).toHaveLength(0);

  // Stops here if there were failures
  // Continues to next action if no failures
  await page.getByRole('button', { name: 'Order History' }).click();
  // ...
});
test('Customer detail screen - verify all fields', async ({ page }) => {
  await page.goto('/customer/123');

  // Create an expect with soft as default
  const softExpect = expect.configure({ soft: true });

  // Use softExpect() instead of expect.soft()
  await softExpect(page.getByTestId('customer-name')).toHaveText('John Smith');
  await softExpect(page.getByTestId('customer-email')).toHaveText('john@example.com');
  await softExpect(page.getByTestId('customer-phone')).toHaveText('555-1234');
});
Scenario Outline: Verify customer detail fields
  Given the user is logged in
  And the customer detail screen is open
  Then the <field> displays "<expected>"

  Examples:
    | field         | expected            |
    | customer name | John Smith          |
    | email         | john@example.com    |
    | phone         | 555-1234            |
    | postal code   | 10001               |
    | status        | Active              |
Then('the {string} displays {string}', async ({ page }, field: string, expected: string) => {
  const testIdMap: Record<string, string> = {
    'customer name': 'customer-name',
    'email': 'customer-email',
    'phone': 'customer-phone',
    'postal code': 'customer-postal',
    'status': 'customer-status',
  };

  const testId = testIdMap[field];
  await expect(page.getByTestId(testId)).toHaveText(expected);
});