Ở bài trước mình đã nói về Page Object Model - cách tổ chức code sao cho gọn gàng. Bài này mình sẽ giới thiệu một công cụ khác cũng rất hữu ích trong automation test: Playwright BDD - thư viện giúp bạn viết test theo phong cách BDD (Behavior Driven Development) bằng ngôn ngữ Gherkin, chạy ngay trên nền Playwright Test. Đây cũng chính là "họ hàng" của Cucumber (bản Gherkin quen thuộc với Java/Selenium) nhưng được tinh chỉnh để chạy trên hạ tầng Playwright.

Bạn thử tưởng tượng: Dev, Tester, BA ngồi họp để chốt yêu cầu cho chức năng "Login". Lúc này BA nói: "Khi user nhập sai mật khẩu thì phải hiện thông báo lỗi". Nhưng vì test nằm trong code nên chỉ có Automation Tester/Dev đọc hiểu được.

test('shows an error when login fails', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await page.goto('https://example.com/login');
  await loginPage.login('wronguser', 'wrongpass');
  await expect(loginPage.errorMessage).toHaveText('Invalid username or password');
});

Nhưng nếu BA, Manual Tester hay người không biết code muốn cùng review testcase thì đoạn code trên khá khó tiếp cận. Sẽ dễ hơn nếu có một cách mô tả behavior của hệ thống bằng ngôn ngữ mà cả technical và non-technical đều có thể đọc hiểu. Đây là nơi Gherkin và Playwright BDD phát huy tác dụng.

Gherkin là gì?

Gherkin là một ngôn ngữ viết kịch bản test theo cấu trúc gần với ngôn ngữ nói, dùng các keyword cố định: Feature, Scenario, Given, When, Then, And, But. Bạn có thể nghĩ Gherkin như một "công thức nấu ăn" - ai đọc vào cũng hiểu các bước cần làm, dù biết nấu ăn hay không.

Ví dụ, kịch bản ở trên viết bằng Gherkin sẽ như thế này:

Feature: Login
  Scenario: Login with invalid credentials
    Given I am on the login page
    When I log in with username "wronguser" and password "wrongpass"
    Then I should see the error message "Invalid username or password"

Nhờ cách mô tả này, BA, Tester và Dev có thể cùng trao đổi về behavior bằng một ngôn ngữ thống nhất và dễ hiểu hơn. Trong BDD, chúng ta hướng đến một shared language dựa trên thuật ngữ nghiệp vụ mà cả team đều hiểu.

Playwright BDD là gì?

Nếu Gherkin là ngôn ngữ để viết kịch bản, thì Playwright BDD là công cụ giúp chạy kịch bản đó như một bài test thật sự trên nền Playwright. Nó đọc file .feature (chứa các dòng Gherkin), rồi tìm đoạn code tương ứng để thực thi từng bước - đoạn code đó gọi là step definition.

Nếu bạn từng nghe tới Cucumber (phổ biến với Java/Selenium) - đây chính là chuẩn mà Playwright BDD dựa theo: cùng cú pháp Gherkin, cùng khái niệm Feature/Scenario/step definition, nhưng Cucumber chạy bằng runner riêng của nó. Trong ví dụ Web Automation này, có thể hình dung Gherkin là "kịch bản", Playwright BDD là "đạo diễn" kết nối kịch bản đó với các hành động thật trên Browser.

Điểm khác biệt kỹ thuật với Cucumber nằm ở cách vận hành bên dưới: Playwright BDD không dựng runner riêng, mà "dịch" file .feature thành test Playwright thật rồi chạy bằng chính Playwright Test. Nhờ vậy nó có sẵn luôn fixture, report, trace viewer, chạy song song... mà không cần dựng lại từ đầu, chỉ đơn giản là tận dụng hạ tầng có sẵn của Playwright.

Cấu trúc một file .feature

  • Feature: mô tả một tính năng lớn (ví dụ: Login, Shopping Cart...)
  • Scenario: một tình huống cụ thể cần kiểm thử (ví dụ: login sai, login đúng...)
  • Given: thiết lập bối cảnh ban đầu ("với điều kiện...")
  • When: hành động người dùng thực hiện ("khi user...")
  • Then: kết quả mong đợi ("thì...")
  • And / But: nối thêm bước, đọc tự nhiên hơn là lặp lại Given/When/Then

Nối tiếp ví dụ ở bài trước, giờ ta viết lại chức năng đăng nhập bằng Playwright BDD. Cấu trúc thư mục sẽ trông như sau:

project-root/
├── features/
│   └── login.feature        # Kịch bản Gherkin
├── fixtures/
│   └── base.fixture.ts      # Khai báo Page Object làm fixture + tạo Given/When/Then
├── steps/
│   └── login.steps.ts        # Step definition
├── pages/
│   └── LoginPage.ts          # Page Object viết ở bài trước
└── playwright.config.ts

Bước 1: Cấu hình playwright.config.ts

Trước khi viết feature/step, cần khai báo defineBddConfig để Playwright BDD biết tìm file .feature và step definition ở đâu:

import { defineConfig } from '@playwright/test';
import { defineBddConfig } from 'playwright-bdd';

const testDir = defineBddConfig({
  features: 'features/*.feature',
  steps: ['steps/*.ts', 'fixtures/*.ts'],
});

export default defineConfig({
  testDir,
});

features trỏ tới các file Gherkin, steps trỏ tới các file chứa Given/When/Then (bao gồm cả fixtures/base.fixture.ts vì đó là nơi createBdd(test) được gọi). defineBddConfig trả về đường dẫn tới thư mục sinh ra file test thật (mặc định .features-gen) - gán thẳng vào testDir để Playwright Test biết chạy ở đâu, mà không cần tạo/khai báo thư mục đó thủ công.

Bước 2: Viết file .feature

features/login.feature:

Feature: Login
  Scenario: Login with invalid credentials
    Given I am on the login page
    When I log in with username "wronguser" and password "wrongpass"
    Then I should see the error message "Invalid username or password"

Bước 3: Khai báo Page Object làm fixture, đồng thời tạo Given/When/Then gắn với fixture đó

fixtures/base.fixture.ts:

import { test as base, createBdd } from 'playwright-bdd';
import { LoginPage } from '../pages/LoginPage';

interface Fixtures {
  loginPage: LoginPage;
}

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

export const { Given, When, Then } = createBdd(test);

Playwright tự tạo page mới và loginPage mới cho mỗi Scenario, rồi tự dọn dẹp sau khi chạy xong - không cần tự quản lý state thủ công. Đặt createBdd(test) ngay trong file khai báo fixture giúp gọn hơn so với tách ra một file riêng, vì Given/When/Then chỉ có ý nghĩa khi đã gắn với đúng test đã compose xong.

Bước 4: Viết step definition - "phiên dịch" từng dòng Gherkin thành code thật

# steps/login.steps.ts

import { expect } from '@playwright/test';
import { Given, When, Then } from '../fixtures/base.fixture';

Given('I am on the login page', async ({ loginPage }) => {
  await loginPage.goto();
});

When('I log in with username {string} and password {string}',
  async ({ loginPage }, username: string, password: string) => {
    await loginPage.login(username, password);
  }
);

Then('I should see the error message {string}',
  async ({ loginPage }, expectedMessage: string) => {
    await expect(loginPage.errorMessage).toHaveText(expectedMessage);
  }
);

Mình vẫn dùng lại LoginPage - Page Object viết ở bài trước! Nhờ loginPage được khai báo là fixture ở Bước 3, mỗi step chỉ cần destructure { loginPage } ra dùng là có ngay instance sẵn sàng, không cần tự quản lý state thủ công. Ngay cả bước điều hướng ban đầu (Given I am on the login page) cũng nên đi qua loginPage.goto() (thêm method goto() vào LoginPage) thay vì gọi thẳng page.goto(), để giữ đúng nguyên tắc chỉ Page Object mới thao tác trực tiếp với UI. Playwright BDD và POM không cạnh tranh nhau, mà bổ trợ cho nhau: Gherkin lo phần "đọc hiểu được", Page Object lo phần "thao tác với UI", còn step definition + fixture là lớp keo dán hai bên lại. Vì step definitions đã đóng vai trò điều phối hành động (tương đương Actions ở bài trước), nên Page Object ở đây có thể gộp luôn action method như login(), không cần tách thêm một lớp Actions riêng.

Bước 5: Generate & chạy test

Playwright BDD cần bước generate file test thật từ .feature trước khi chạy, sau đó mới chạy như một test Playwright bình thường:

pnpm exec bddgen
pnpm exec playwright test

bddgen đọc cấu hình defineBddConfig ở Bước 1 để biết vị trí .feature/step definition, rồi sinh file spec vào testDir (mặc định .features-gen). playwright test sau đó chỉ chạy đúng những file đã sinh sẵn trong testDir này - nó không tự đọc .feature hay biết Given/When/Then là gì.

Vì vậy, mỗi khi sửa .feature hoặc step definition mà quên chạy lại bddgen, playwright test vẫn sẽ chạy đúng bản build cũ: thay đổi mới coi như "vô hình", dễ tạo ảo giác test đã pass trong khi thực chất chưa test logic mới. Nặng hơn, nếu bạn đổi tên step text nhưng bản build cũ vẫn gọi tên cũ, sẽ gặp lỗi runtime "step is not defined".

Gọn hơn, có thể gộp thành một script trong package.json để khỏi quên bước generate:

{
  "scripts": {
    "test:bdd": "bddgen && playwright test"
  }
}

rồi chạy bằng pnpm test:bdd.

Mẹo nhỏ khi áp dụng

  1. Viết Scenario theo góc nhìn người dùng, không theo góc nhìn kĩ thuật. Nên viết "When I click the Login button" thay vì "I click the element with id loginBtn" - vì mục tiêu là ai đọc cũng hiểu. Gherkin nên tập trung vào behavior thay vì chi tiết implementation
  2. Đừng lạm dụng Given quá nhiều bước nhỏ lẻ. Nếu Given có tới 5-6 bước rời rạc, hãy gộp lại thành 1 câu có ý nghĩa nghiệp vụ rõ ràng hơn, ví dụ gộp thành Given I am logged in as an admin.
  3. Tái sử dụng khi các step thực sự có cùng ý nghĩa, không cần cố gộp mọi step chỉ để giảm số lượng code. Lưu ý step definitions được match theo namespace chung của cả project - nếu 2 feature khác nhau viết step gần giống nhau, có thể báo lỗi "Multiple step definitions match" hoặc match nhầm step. Nên đặt tên step rõ nghĩa, tránh trùng lặp mơ hồ giữa các feature.
  4. Kết hợp Background khi nhiều Scenario trong cùng 1 Feature đều cần chung bước thiết lập ban đầu (ví dụ luôn cần "Given I am on the login page"), tránh lặp lại ở từng Scenario.
  5. Dùng Scenario Outline + Examples khi muốn chạy cùng 1 kịch bản với nhiều bộ dữ liệu khác nhau, ví dụ thử nhiều cặp username/password sai khác nhau mà không cần viết lại nhiều Scenario:
Scenario Outline: Login fails with various invalid inputs
  Given I am on the login page
  When I log in with username "<username>" and password "<password>"
  Then I should see the error message "<message>"

  Examples:
    | username  | password  | message |
    | wronguser | wrongpass | Invalid username or password |
    |           | abcd123   | Username is required |
    | admin     |           | Password is required |

Vì sao nên dùng Gherkin/Playwright BDD?

  • Ai cũng đọc hiểu được test case - kể cả BA, khách hàng không biết code.
  • Hỗ trợ cách làm BDD - Gherkin có thể được dùng để ghi lại những behavior/example mà BA, Dev và Tester đã cùng thống nhất, rồi kết nối những kịch bản này với automation code.
  • Tái sử dụng step - một step như Given I am on the login page có thể dùng lại ở rất nhiều scenario khác nhau.
  • Tài liệu sống - file .feature vừa là test, vừa là tài liệu mô tả hệ thống hoạt động thế nào
  • Tận dụng hạ tầng Playwright có sẵn - report, trace viewer, chạy song song, retry... đều dùng được ngay mà không cần cấu hình thêm, vì bên dưới vẫn là Playwright Test.