MAISON CODE .
/ Tech · Testing · QA · Playwright · CI/CD · Engineering Culture

Automated Testing: The Foundation of Engineering Speed

Manual testing is slow, expensive, and unreliable. How to build a robust End-to-End (E2E) testing strategy using Playwright and GitHub Actions.

AB
Alex B.
Automated Testing: The Foundation of Engineering Speed

The Fear of Deployment

Every software engineer knows the feeling. It is Friday, 4:00 PM. You have just merged a Pull Request. The deployment pipeline turns green. You should be happy. But you are sweating. “Did I break the checkout flow?” “Did I break the password reset?” “Did I test the mobile menu on Safari?” If you rely on Manual Testing (clicking around the website yourself), you are living in a constant state of fear. Humans are terrible at regression testing. We get bored. We miss things. We test the “Happy Path” and forget the edge cases. As the codebase grows, the time required to manually test it grows linearly. Eventually, you stop testing. That is when the bugs leak into Production. And when bugs leak, you lose revenue. You lose trust. Automated Testing is the cure for this fear. It turns deployment from a “High Risk Event” into a “Non-Event”.

Why Maison Code Discusses This

At Maison Code, we work with high-stakes luxury brands and heavy-traffic e-commerce platforms. Our clients generate thousands of euros per minute during peak sales (Black Friday, Christmas). A “minor” bug in the checkout flow is not an inconvenience; it is a financial catastrophe. We have seen brands lose $50k in an hour because a “AddToCart” button was covered by a Z-Index error on iPhone 12. We talk about Automated Testing because Stability is Revenue. We do not sell “code”. We sell “Reliability”. Implementing a robust E2E suite is often the first thing we do when auditing a client’s legacy codebase. It stops the bleeding and allows us to refactor with confidence.

1. The Testing Pyramid: A Strategic Framework

Mike Cohn popularized the “Testing Pyramid” concept, and it remains the gold standard for testing strategy. It defines the ideal distribution of tests in your suite.

  1. Unit Tests (70%):

    • Scope: Individual functions or classes.
    • Speed: Milliseconds.
    • Cost: Cheap.
    • Tool: Jest, Vitest.
    • Example: add(2, 2) === 4.
  2. Integration Tests (20%):

    • Scope: Interactions between components (e.g., Parent passing props to Child).
    • Speed: Seconds.
    • Cost: Medium.
    • Tool: React Testing Library.
  3. End-to-End (E2E) Tests (10%):

    • Scope: Full user flows in a real browser.
    • Speed: Minutes.
    • Cost: Expensive (Compute heavy).
    • Tool: Playwright, Cypress.

In this article, we will focus on the top of the pyramid: E2E Testing. Why? Because it provides the highest Confidence Coefficient. A unit test can pass even if the “Submit” button is hidden by a CSS z-index bug. An E2E test will fail, because it tries to click the button. If an E2E test says “User purchased item”, you can be 99.9% sure that users can purchase items.

2. The Tool of Choice: Playwright

For a decade, Selenium was the standard. It was slow, Java-based, and notoriously flaky. Then came Cypress. It was a massive improvement, with a great developer experience, but it had architectural limitations (running inside the browser sandbox, limited multi-tab support). Today, the industry standard is Playwright (by Microsoft). Why Playwright?

  1. Speed: It runs tests in parallel across multiple worker processes.
  2. Reliability: It has “Auto-Waiting”. It waits for elements to be actionable before interacting. No more sleep(1000).
  3. Multi-Browser: It tests natively on Chromium, Firefox, and WebKit (Safari).
  4. Tracing: It records a full video and DOM trace of every failure, making debugging trivial.

3. Implementation: Testing a Checkout Flow

Let’s look at a real-world example. We want to test that a guest user can purchase a product.

// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  
  // Isolate the test environment
  test.beforeEach(async ({ page }) => {
    // Reset cookies/storage
    await page.context().clearCookies();
  });

  test('Guest user can purchase an item', async ({ page }) => {
    // 1. Navigation
    console.log('Navigating to Product Page...');
    await page.goto('/products/silk-shirt');
    await expect(page).toHaveTitle(/Silk Shirt/);
    
    // 2. Add to Cart
    // Use user-facing locators (Role, Label, Text)
    // Avoid CSS selectors like '.btn-primary' (fragile)
    await page.getByRole('button', { name: 'Add to Cart' }).click();
    
    // 3. Verify Cart Drawer
    const cartDrawer = page.getByTestId('cart-drawer');
    await expect(cartDrawer).toBeVisible();
    await expect(cartDrawer).toContainText('Silk Shirt');
    
    // 4. Proceed to Checkout
    await page.getByRole('link', { name: 'Checkout' }).click();
    await expect(page).toHaveURL(/.*\/checkout/);
    
    // 5. Fill Form (Mocking Payment)
    await page.getByLabel('Email').fill('test-bot@maisoncode.paris');
    await page.getByLabel('First Name').fill('Test');
    await page.getByLabel('Last Name').fill('Bot');
    await page.getByLabel('Address').fill('123 Test St');
    
    // 6. Payment (Stripe Mock)
    // In strict E2E, we might use a Stripe Test Card.
    await page.getByLabel('Card number').fill('4242 4242 4242 4242');
    await page.getByLabel('Expiry').fill('12/30');
    await page.getByLabel('CVC').fill('123');
    
    // 7. Submit
    await page.getByRole('button', { name: 'Pay Now' }).click();
    
    // 8. Assert Success
    // Increase timeout because payment processing takes time
    await expect(page.getByText('Thank you for your order')).toBeVisible({ timeout: 15000 });
  });

});

4. Handling “Flakiness” (The Silent Killer)

A Flaky Test is a test that passes 90% of the time and fails 10% of the time, without any code changes. Flakiness is the enemy. If developers stop trusting the tests (“Oh, just re-run it, it’s flaky”), the testing suite becomes useless.

Common Causes:

  1. Network Latency: API taking 5.1s when timeout is 5s.
  2. Animation: Clicking a button while it is still sliding in.
  3. Data Contamination: Test A deletes a user that Test B needs.

Solutions:

  1. Mocking (Network Interception): Instead of calling the real Contentful API (which might be slow), intercept the request and return a static JSON.
    await page.route('**/api/products', route => {
      route.fulfill({ path: 'mock-data/products.json' });
    });
    This makes the test 10x faster and 100% deterministic.
  2. Retries: Configure CI to retry failed tests automatically. retries: 2. If it passes on retry, it’s flaky, but at least it doesn’t block deployment.

5. Visual Regression Testing (Pixel Perfect)

Playwright checks functionality (“Is the button clickable?”). It does not check aesthetics (“Is the button pink?”). If you accidentally delete main.css, Playwright might still pass (the button is clickable, just invisible). Visual Regression Testing (Percy / Chromatic / Playwright Snapshots) fixes this. await expect(page).toHaveScreenshot();

  1. Take a screenshot of the page.
  2. Compare it to the “Golden Image” (Baseline).
  3. If pixels differ by > 1%, fail the test. This catches “CSS Regressions” that no functional test ever could.

6. GitHub Actions: The Automated Gatekeeper

You don’t run tests on your laptop. You run them on CI (Continuous Integration). Every time you push to GitHub, a workflow runs.

# .github/workflows/e2e.yml
name: Playwright E2E
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Install Deps
        run: npm ci
      - name: Install Browsers
        run: npx playwright install --with-deps
      
      - name: Run Playwright
        run: npx playwright test
        env:
          CI: true
          BASE_URL: ${{ github.event.deployment.payload.web_url }} // Test the Vercel Preview URL

      - name: Upload Report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

7. The Cost of CI (Optimization)

Running 500 E2E tests on every commit is expensive (GitHub Actions minutes). Optimization Strategies:

  1. Sharding: Split tests across 10 machines causing them to run 10x faster. npx playwright test --shard=1/10.
  2. Affected Projects Only: Use Nx or Turbo to only run tests for apps that changed.
  3. Smoke Tests for PRs: Run a small subset (Critical Path) on PRs. Run the full suite on Main.

8. Conclusion

Automated Testing is not “extra work”. It is the work. It is the difference between a prototype and a product. It is the difference between “I hope it works” and “I know it works”. Start today. Write ONE test. The Login test. Then the Checkout test. Soon, you will sleep better at night.


Releases causing panic?

We architect automated E2E testing suites using Playwright that catch bugs before users do.

Hire our Architects.