Skip to content
GeeksSmith
advanced 8 min read

Frontend Testing Strategies: Trophy, RTL, MSW & Playwright

A modern frontend testing strategy follows the Testing Trophy (static analysis, unit tests, integration tests via RTL + MSW, and E2E smoke tests via Playwright) to maximize confidence while minimizing maintenance overhead.

Why it matters in interviews

Lead candidates are frequently asked: 'How do you test components without testing implementation details?', 'How do you mock API layers reliably?', and 'How do you set up CI test gates that run in under 3 minutes?'.

Visual & Interactive Explanation

Testing Pyramid vs Modern Testing Trophy

Interactive Comparison

Traditional Pyramid

Unit-heavy legacy approach

Outdated
FocusUnit tests (70%)
MockingHeavy internal mocks (shallow rendering)
FlakinessHigh refactor brittleness
ConfidenceLow user-behavior assurance

Testing Trophy

Integration-heavy modern standard

Industry Standard
FocusIntegration tests (60%)
MockingNetwork boundary only (MSW)
FlakinessExtremely resilient to refactoring
ConfidenceHigh real-world confidence

Code Examples & Implementation

RTL + userEvent + Accessible Role Queries
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

test("submits credentials and displays welcome message", async () => {
  const user = userEvent.setup();
  render(<LoginForm onLoginSuccess={vi.fn()} />);

  // Query by accessible roles (never use classnames or component state)
  const usernameInput = screen.getByRole("textbox", { name: /username/i });
  const passwordInput = screen.getByLabelText(/password/i);
  const submitButton = screen.getByRole("button", { name: /sign in/i });

  // Simulate real user keystrokes and click
  await user.type(usernameInput, "alice@example.com");
  await user.type(passwordInput, "Secret123!");
  await user.click(submitButton);

  // Assert observable DOM changes
  expect(
    await screen.findByRole("heading", { name: /welcome back, alice/i })
  ).toBeInTheDocument();
});
Mock Service Worker (MSW) Network Mock Handler
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";

export const handlers = [
  // Intercept GET /api/user
  http.get("/api/user", () => {
    return HttpResponse.json({ id: "1", name: "Alice", role: "admin" });
  }),

  // Intercept POST /api/checkout with failure override in specific test
  http.post("/api/checkout", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ orderId: "ORD-999", status: "confirmed" });
  }),
];

export const server = setupServer(...handlers);
Playwright Page Object Model (POM) E2E Test
import { test, expect } from "@playwright/test";

export class CheckoutPage {
  constructor(private page: import("@playwright/test").Page) {}

  async navigate() {
    await this.page.goto("/checkout");
  }

  async fillShippingDetails(address: string) {
    await this.page.getByRole("textbox", { name: /address/i }).fill(address);
  }

  async placeOrder() {
    await this.page.getByRole("button", { name: /place order/i }).click();
  }
}

test("completes checkout journey", async ({ page }) => {
  const checkout = new CheckoutPage(page);
  await checkout.navigate();
  await checkout.fillShippingDetails("123 Main St");
  await checkout.placeOrder();

  await expect(page.getByText(/order confirmed/i)).toBeVisible();
});

Interview-Ready Answers

Follow the Testing Trophy: prioritize integration tests with React Testing Library and MSW simulating real user interactions, backed by static TypeScript/ESLint checks and fast Playwright E2E smoke journeys.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1medium
// Given this DOM:
// <button aria-label="Close dialog">X</button>

// What is the accessible query to find this button?
screen.getByRole("button", { name: "Close dialog" });
// Does screen.getByRole("button", { name: "X" }) work?

Common Mistakes & Anti-Patterns

  • Testing internal component state (e.g. `wrapper.state('open')`) rather than user-visible DOM changes
  • Using `fireEvent.click()` instead of `@testing-library/userEvent` (skipping focus, blur, and pointer events)
  • Mocking sub-components instead of testing them together in realistic integration tests
  • Relying on brittle CSS selectors (e.g. `.container > div:nth-child(2)`) instead of accessible roles

Real-World Architectural Scenario

A team refactors a form component from 5 `useState` hooks to a single `useReducer`. 40 unit tests fail even though the UI works identically. Why did this happen and how do you prevent it?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: