Tools & Workflow

Playwright E2E Testing: Viết Test Bền Vững Cho Ứng Dụng Next.js

By Ginbok7 min read

Hãy tưởng tượng bạn vừa xây dựng xong một ứng dụng web tuyệt vời bằng Next.js. Nó trông rất đẹp, nhưng làm sao bạn biết chắc chắn nó hoạt động đúng cho người dùng? Kiểm thử thủ công (manual testing) thường nhàm chán và dễ sai sót. Đó là lý do End-to-End (E2E) testing ra đời, và Playwright hiện là công cụ hàng đầu cho nhiệm vụ này.

E2E Testing thực chất là gì?

Hãy coi E2E testing như một "Khách hàng Robot". Thay vì chỉ kiểm tra từng đoạn code nhỏ rải rác (như Unit testing), E2E test sẽ mô phỏng lại chính xác hành trình của một người dùng thực sự. Nó sẽ tự động mở trình duyệt (Chrome, Safari, Edge), click vào các nút bấm, điền form, và xác nhận rằng ứng dụng phản hồi chính xác theo góc nhìn của người dùng.

Playwright (một công cụ mạnh mẽ do Microsoft phát triển) giúp thực hiện điều này cực kỳ nhanh chóng và ổn định. Hãy cùng xem cách chúng tôi áp dụng nó vào thực tế trong dự án Ginbok CMS Next.js nhé.

Vấn đề: Những bài Test dễ vỡ (Fragile Tests)

Trước đây, khi viết test cho frontend Next.js, chúng tôi thường gán chặt (coupling) mã test với các class CSS cụ thể. Ví dụ, để test trang danh sách blog, code có thể trông như sau:


// Ví dụ về test dễ vỡ
const blogItems = page.locator('.blog-card-item'); // NGUY HIỂM!
await expect(blogItems).toBeVisible();

Điều gì xảy ra nếu chúng ta thiết kế lại ứng dụng và chuyển sang một theme mới (như theme Broadsheet thanh lịch của chúng tôi)? Class CSS sẽ thay đổi thành .article-item, và bùm... toàn bộ test của chúng ta sẽ thất bại (Fail), mặc dù tính năng hiển thị bài viết vẫn hoạt động hoàn hảo!

Giải pháp: Test không phụ thuộc Theme (Theme-Agnostic Tests)

Để khắc phục triệt để vấn đề này trong dự án Ginbok, chúng tôi đã viết lại toàn bộ kịch bản Playwright (cho trang chủ, danh sách blog và tìm kiếm) sử dụng Role-based và Text-based Selectors. Nhờ vậy, các test này trở nên "mù màu" với theme — chúng chỉ kiểm tra cấu trúc ngữ nghĩa (semantic) của DOM, chứ không bận tâm đến style giao diện.

Ví dụ 1: Test bền vững cho trang danh sách Blog

Dưới đây là cách chúng tôi kiểm tra tính năng "Filter Tabs" (Tab danh mục) và "Phân trang" mà không cần dựa vào bất kỳ class CSS dễ vỡ nào:


import { test, expect } from '@playwright/test';

test.describe('Blog Listing Page', () => {
    test('should display filter tabs resiliently', async ({ page }) => {
        await page.goto('/blog');

        // ✅ TỐT: Sử dụng ARIA roles thay vì CSS classes
        const tablist = page.locator('[role="tablist"]');
        await expect(tablist).toBeVisible();

        const tabs = page.locator('[role="tab"]');
        const count = await tabs.count();
        expect(count).toBeGreaterThanOrEqual(2); // Ít nhất phải có "All" + 1 danh mục
    });

    test('should navigate via pagination', async ({ page }) => {
        await page.goto('/blog');

        // ✅ TỐT: Truy xuất qua nhãn hỗ trợ tiếp cận (accessibility labels)
        const pagination = page.getByLabel('Pagination');

        // ✅ TỐT: Truy xuất qua nội dung text hiển thị
        const page2Link = pagination.getByText('02');
        await page2Link.click();

        await page.waitForURL(/p=2/);
        expect(page.url()).toContain('p=2');
    });
});

Ví dụ 2: Test bền vững cho tính năng Tìm kiếm

Tìm kiếm là một hành trình người dùng quan trọng. Đây là cách test mà không phụ thuộc vào bất kỳ CSS class hay markup riêng của theme:


import { test, expect } from '@playwright/test';

test.describe('Search Page', () => {
    test('should return results for a valid query', async ({ page }) => {
        await page.goto('/search?q=playwright');

        // ✅ TỐT: Tìm ô input bằng ARIA role + label
        const searchInput = page.getByRole('searchbox', { name: /search/i });
        await expect(searchInput).toHaveValue('playwright');

        // ✅ TỐT: Kiểm tra kết quả qua semantic landmark, không dùng CSS class
        const resultsList = page.getByRole('list', { name: /search results/i });
        await expect(resultsList).toBeVisible();

        const items = resultsList.getByRole('listitem');
        const count = await items.count();
        expect(count).toBeGreaterThan(0);
    });

    test('should show empty state for no results', async ({ page }) => {
        await page.goto('/search?q=xyznonexistentkeyword');

        // ✅ TỐT: Dùng text hiển thị để kiểm tra trạng thái "không có kết quả"
        const emptyMessage = page.getByText(/no results found/i);
        await expect(emptyMessage).toBeVisible();
    });
});

Tại sao điều này quan trọng cho ứng dụng Next.js của bạn?

  1. Tự tin đổi Theme: Bạn có thể vứt bỏ toàn bộ file CSS cũ và các test vẫn Pass xanh rì, vì cấu trúc HTML gốc (như thẻ <article>, thẻ <h1>, hay role="tab") vẫn được giữ nguyên.
  2. Cải thiện Accessibility (A11y): Bằng cách ép bản thân viết test dựa trên các thuộc tính ARIA, bạn đang gián tiếp buộc mình phải xây dựng một ứng dụng thân thiện hơn với trình đọc màn hình.
  3. Bảo trì bằng 0: Bạn sẽ dành thời gian để phát triển tính năng mới, thay vì suốt ngày đi sửa test chỉ vì một anh designer nào đó đổi tên class của cái nút bấm.

Quy trình làm việc mới khi có Playwright E2E


❌ Quy trình cũ (không có E2E):
   Viết code → Click thủ công trên trình duyệt → "Có vẻ ổn rồi"
             → Deploy → User báo bug trên production → Panic hotfix

✅ Quy trình mới (có Playwright):
   Viết code → npx playwright test (chạy local, ~30 giây)
             → Test bắt lỗi regression ngay lập tức
             → Mở PR → CI chạy toàn bộ E2E suite trên mỗi push
             → Chỉ deploy khi tất cả xanh ✅
             → Ship với sự tự tin

Bước 1 — Xác định: cần viết spec mới hay cập nhật spec cũ?


1. Có URL hoặc hành trình người dùng mới không?  → Tạo spec file mới
2. Có flow hiện tại thay đổi không?              → Cập nhật spec cũ
3. Chỉ thêm UI không liên quan (màu sắc, widget, sidebar)?
                                                  → Không cần đụng spec

Nguyên tắc chung: một tính năng chỉ được coi là done khi spec của nó đã tồn tại và pass — không phải sau khi deploy xong mới nghĩ đến test.

Bước 2 — Chạy local bằng UI mode trong lúc phát triển

Dùng lệnh npx playwright test --ui để mở giao diện tương tác của Playwright. Bạn có thể quan sát "Khách hàng Robot" điều hướng ứng dụng theo thời gian thực và thấy chính xác nơi nó thất bại.

Bước 3 — Chặn mọi PR bằng CI

Thêm Playwright vào pipeline CI (GitHub Actions, Azure DevOps, v.v.) để test tự động chạy trên mỗi pull request. Một test thất bại sẽ chặn việc merge.


# Ví dụ: Các bước pipeline trên Azure DevOps
- script: npm ci
  displayName: 'Cài đặt dependencies'

- script: npx playwright install --with-deps
  displayName: 'Cài đặt Playwright browsers'

- task: Npm@1
  inputs:
    command: 'custom'
    customCommand: 'exec playwright test --reporter=junit'
  displayName: 'Chạy Playwright E2E tests'

- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: 'test-results/*.xml'
  displayName: 'Publish kết quả test'

Bước 4 — Dùng HTML report để debug lỗi

Khi một test thất bại trên CI, Playwright tự động sinh ra HTML report chi tiết kèm screenshot, video và trace. Chạy npx playwright show-report trên máy local để đi từng bước qua đúng khoảnh khắc lỗi xảy ra.

Kết quả? Team bạn ship nhanh hơn, người dùng gặp ít bug hơn, và việc đổi theme trở thành chuyện bình thường thay vì một cơn ác mộng kiểm thử.

Bạn đã sẵn sàng tận tay trải nghiệm? Chỉ cần gõ lệnh npx playwright test --ui và chiêm ngưỡng "Khách hàng Robot" của bạn duyệt web tự động ngay trên màn hình!

#Playwright#E2E Testing#Next.js#Automation#TypeScript#ci-cd#accessibility#testing
← Back to Articles
Playwright E2E Testing: Viết Test Bền Vững Cho Ứng Dụng Next.js - Ginbok