QA Interview Hub · Knowledge base · Quizzes

Patterns

Facade, Steps and App Actions

Facade and business steps

A Facade gives a simple interface to a complex subsystem. In tests it is a "steps" layer that combines several Page Objects and API calls into one business action: "make a transfer", "pass KYC".

export class TransferSteps {
  constructor(
    private readonly accounts: AccountsPage,
    private readonly transfer: TransferPage,
  ) {}

  async sendMoney(from: string, to: string, amount: number) {
    return test.step(`send ${amount} from ${from} to ${to}`, async () => {
      await this.accounts.open(from);
      await this.accounts.clickTransfer();
      await this.transfer.fill(to, amount);
      await this.transfer.confirm();
    });
  }
}

test.step groups actions in the report and trace, so a failure is seen as "send 100 from A to B", not as a click without context. test.step returns the callback's value. The option { box: true } shows the place of the error at the step's call line, not inside.

When NOT to: hide in a Facade what the test verifies. If a test is about validation of the transfer form, it must work with TransferPage directly. A Facade is needed for preconditions and repeated end-to-end paths.

A typical mistake: layers Test -> Steps -> Actions -> Pages -> Components, where each layer just forwards the call. That is not abstraction but indirection, which is hard to debug.

In an interview: A Facade is usually a steps class that combines several pages into one business action, wrapped in test.step for the report. It is used for preconditions, and what the test verifies stays visible in the test itself.

App Actions: preconditions without the UI

App Actions (a term popularised by Cypress) means preparing state through the API, the DB or the application itself, not by clicking the UI. For example, login through the UI in every test is slow and brittle, though the test is about something quite different.

Playwright has several mechanisms for this:

  • storageState. A setup project logs in once and saves cookies and localStorage to a file, and other projects pick it up through use: { storageState }.
  • The request fixture for creating data through the API before a test.
  • page.route for replacing backend responses.
// auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ request }) => {
  await request.post('/api/login', {
    data: { email: 'qa@test.com', password: process.env.QA_PASS },
  });
  await request.storageState({ path: '.auth/user.json' });
});

In the config this setup is declared as a separate project, and the main project lists it in dependencies and uses storageState: '.auth/user.json'.

When NOT to: for the login test itself, because there the UI is exactly what is being verified. You also need at least a few E2E tests that go through the key path fully through the UI.

In an interview: I prepare preconditions through the API and storageState, not through the UI, because it is many times faster and more stable. Through the UI I go only through what the test really verifies, plus a few end-to-end smoke scenarios.