Danh sách bài viết

Bài 59: testMatch & testIgnore Per Project

Mỗi project trong playwright.config.ts có thể khai báo testMatch và testIgnore riêng để scope file test — chạy đúng loại file cho đúng project mà không cần folder tách biệt. Bài này đi vào 3 format value (string glob, array glob, regex), cơ chế override top-level, combine với testDir, 4 use case thực tế, 4 pitfall quan trọng và quiz 5 câu.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau khi hoàn thành bài này, bạn sẽ:

  • Phân biệt được 3 format value của testMatchtestIgnore: string glob, array glob và regex.
  • Hiểu cách override top-level testMatch per project và khi nào project-level thắng.
  • Áp dụng được pattern tách unit/integration/e2e, setup/teardown, domain-driven và smoke subset.
  • Combine testDir với testMatch để scope file trong folder con.
  • Tránh 4 pitfall thực tế: regex escape, glob * vs **, file overlap và pattern stale.
2

testMatch Và testIgnore Là Gì

testMatch là pattern xác định những file nào được xem là test file của project. Playwright chỉ load và chạy các file khớp pattern đó. testIgnore là pattern loại trừ — file khớp testIgnore sẽ không được chạy dù khớp testMatch.

Khi được khai báo trong projects[], hai option này hoạt động ở scope project — mỗi project có thể có tập file riêng biệt dù tất cả test nằm cùng một thư mục.

Điểm khác biệt quan trọng so với folder-based split: bạn không cần di chuyển file vào thư mục riêng cho từng project. Naming convention của file (ví dụ .unit.spec.ts, .e2e.spec.ts) là cơ sở để tách project.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'unit',
      testMatch: /.*\.unit\.spec\.ts$/,       // chỉ file .unit.spec.ts
    },
    {
      name: 'integration',
      testMatch: /.*\.integration\.spec\.ts$/, // chỉ file .integration.spec.ts
    },
    {
      name: 'e2e',
      testIgnore: /.*\.(unit|integration)\.spec\.ts$/, // tất cả trừ unit và integration
    },
  ],
});

Project e2e không khai báo testMatch — nó dùng default match (**/*.@(spec|test).?(c|m)[jt]s?(x)) rồi loại trừ file unit và integration bằng testIgnore.

3

3 Format Value: Glob, Array, Regex

testMatchtestIgnore đều nhận một trong 3 dạng:

String glob

Pattern dạng glob, dùng cú pháp micromatch. Match theo đường dẫn tương đối từ rootDir (hoặc testDir nếu được khai báo):

// Chỉ file .spec.ts trong mọi subfolder
testMatch: '**/*.spec.ts'

// Chỉ file trong folder tests/smoke, không đệ quy sâu hơn
testMatch: 'tests/smoke/*.spec.ts'

// File bắt đầu bằng "auth."
testMatch: '**/auth.*.spec.ts'

Array of glob

Dùng array khi cần nhiều pattern include (OR logic — file khớp bất kỳ pattern nào trong array đều được chạy):

testMatch: [
  '**/*.spec.ts',
  '**/*.test.ts',
]

// Nhiều folder hoặc naming convention khác nhau
testMatch: [
  'tests/api/**/*.spec.ts',
  'tests/api/**/*.test.ts',
]

RegExp

Match bằng biểu thức chính quy trên đường dẫn đầy đủ (absolute path) của file:

// File kết thúc bằng .unit.spec.ts
testMatch: /.*\.unit\.spec\.ts$/

// File trong thư mục admin (bất kỳ độ sâu)
testMatch: /\/admin\//

// File có tên chứa "smoke"
testMatch: /smoke.*\.spec\.ts$/

Regex match trên absolute path của file, không phải relative path. Điều này có nghĩa là pattern /tests\// sẽ khớp bất kỳ file nào có segment tests/ trong đường dẫn đầy đủ, bao gồm cả /home/user/project/tests/auth.spec.ts.

testIgnore nhận cùng 3 format và hoạt động theo cơ chế đối nghịch: file khớp pattern sẽ bị loại trừ.

4

Default testMatch Và Override Per Project

Khi không khai báo testMatch ở bất kỳ đâu, Playwright dùng default pattern:

**/*.@(spec|test).?(c|m)[jt]s?(x)

Pattern này match các file có dạng:

  • *.spec.ts, *.test.ts
  • *.spec.js, *.test.js
  • *.spec.tsx, *.test.tsx
  • *.spec.mts, *.spec.cts, *.spec.mjs, v.v.

Khi khai báo testMatch ở top-level config, tất cả project dùng giá trị đó làm default — trừ project nào tự khai báo testMatch riêng. Project-level testMatch thắng top-level hoàn toàn (không merge, chỉ replace):

export default defineConfig({
  // Top-level: match .spec.ts và .test.ts
  testMatch: ['**/*.spec.ts', '**/*.test.ts'],

  projects: [
    {
      name: 'general',
      // Không khai báo testMatch → dùng top-level: *.spec.ts và *.test.ts
    },
    {
      name: 'special',
      // Project-level thắng, top-level bị bỏ qua hoàn toàn cho project này
      testMatch: 'special/*.ts',
    },
  ],
});

Project special chỉ chạy file trong folder special/ với extension .ts bất kỳ — kể cả file không có suffix .spec. Top-level pattern ['**/*.spec.ts', '**/*.test.ts'] không còn áp dụng cho project này.

5

Use Case: Tách Unit / Integration / E2E

Đây là use case phổ biến nhất: mỗi loại test đặt tên file theo suffix riêng, mỗi project chỉ chạy suffix của mình.

Naming convention của file test:

  • auth.unit.spec.ts — unit test, không cần browser thật
  • auth.integration.spec.ts — integration test, thường dùng 1 browser
  • checkout.e2e.spec.ts — end-to-end, thường chạy multi-browser
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'unit',
      testMatch: /.*\.unit\.spec\.ts$/,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'integration',
      testMatch: /.*\.integration\.spec\.ts$/,
      use: { ...devices['Desktop Chrome'] },
    },
    // E2E chạy multi-browser — 3 project cùng testMatch
    {
      name: 'e2e-chromium',
      testMatch: /.*\.e2e\.spec\.ts$/,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'e2e-firefox',
      testMatch: /.*\.e2e\.spec\.ts$/,
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'e2e-webkit',
      testMatch: /.*\.e2e\.spec\.ts$/,
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Chạy theo loại:

# Chỉ unit test
npx playwright test --project=unit

# Chỉ integration
npx playwright test --project=integration

# Chỉ e2e trên Chromium
npx playwright test --project=e2e-chromium

# Chạy tất cả e2e (mọi browser)
npx playwright test --project='e2e-*'

File auth.unit.spec.ts sẽ chỉ chạy trong project unit. File checkout.e2e.spec.ts sẽ chạy 3 lần — mỗi lần trong 1 browser project e2e.

6

Use Case: Setup Và Teardown Project

Các file .setup.ts.teardown.ts thực chất là test file có logic đặc biệt (tạo session, dọn data). Chúng cần chạy theo project riêng với dependenciesteardown — không được lẫn vào project test thông thường.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    // Project chạy setup trước
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts$/,
    },
    // Project chạy teardown sau
    {
      name: 'teardown',
      testMatch: /.*\.teardown\.ts$/,
    },
    // Project test chính — loại trừ cả setup lẫn teardown
    {
      name: 'main',
      testMatch: /.*\.spec\.ts$/,
      testIgnore: [/.*\.setup\.ts$/, /.*\.teardown\.ts$/],
      dependencies: ['setup'],
      teardown: 'teardown',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

Nếu không khai báo testIgnore trong project main, Playwright sẽ không tự biết rằng auth.setup.ts không phải test thông thường — nó vẫn khớp testMatch: /.*\.spec\.ts$/? Không — nhưng nếu bạn dùng testMatch: '**/*.ts' hoặc không khai báo gì, file .setup.ts sẽ lọt vào. Explicit testIgnore giải quyết ambiguity này.

Naming convention khuyến nghị:

  • Setup file: auth.setup.ts, data.setup.ts
  • Teardown file: cleanup.teardown.ts
  • Test file: login.spec.ts, checkout.spec.ts
7

Use Case: Domain-driven Với testDir

Khi tổ chức test theo domain (admin, user, checkout, search), mỗi domain có folder riêng. testDir xác định base folder, testMatch xác định pattern bên trong folder đó:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'admin',
      testDir: 'tests/admin',
      testMatch: '*.spec.ts',  // Glob đơn giản — tìm trong testDir
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'auth/admin.json',
      },
    },
    {
      name: 'user',
      testDir: 'tests/user',
      testMatch: '*.spec.ts',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'auth/user.json',
      },
    },
    {
      name: 'api',
      testDir: 'tests/api',
      testMatch: ['*.spec.ts', '*.test.ts'],  // Array — 2 naming convention
    },
  ],
});

Khi testDir được khai báo trong project, glob pattern trong testMatch được resolve tương đối từ testDir đó. Glob '*.spec.ts' (không có **) chỉ match file ngay trong testDir, không đệ quy vào subfolder. Dùng '**/*.spec.ts' để match đệ quy:

// tests/admin/
//   ├── dashboard.spec.ts          ← match '*.spec.ts'
//   ├── users.spec.ts              ← match '*.spec.ts'
//   └── reports/
//       └── monthly.spec.ts        ← KHÔNG match '*.spec.ts', cần '**/*.spec.ts'

{
  name: 'admin',
  testDir: 'tests/admin',
  testMatch: '**/*.spec.ts',  // đệ quy — match cả reports/monthly.spec.ts
}

Pattern domain-driven không cần testMatch nếu tất cả file trong folder đều là test:

projects: [
  // Mỗi project chỉ cần testDir — tất cả .spec.ts trong folder đó đều chạy
  { name: 'auth',     testDir: 'tests/auth' },
  { name: 'checkout', testDir: 'tests/checkout' },
  { name: 'search',   testDir: 'tests/search' },
],

Chạy theo domain:

npx playwright test --project=checkout  # chỉ tests/checkout/
8

Use Case: Smoke Subset Project

Smoke test thường là tập nhỏ test quan trọng nhất, chạy nhanh sau mỗi deploy. Có thể tổ chức bằng subfolder hoặc naming suffix:

Cách 1: Subfolder

// tests/
//   smoke/
//     login.spec.ts
//     homepage.spec.ts
//   full/
//     checkout.spec.ts
//     ...

projects: [
  {
    name: 'smoke',
    testDir: 'tests/smoke',
  },
  {
    name: 'full',
    testDir: 'tests/full',
  },
],

Cách 2: Naming suffix

// tests/
//   login.smoke.spec.ts
//   login.spec.ts         ← full test
//   checkout.smoke.spec.ts
//   checkout.spec.ts

projects: [
  {
    name: 'smoke',
    testMatch: /.*\.smoke\.spec\.ts$/,
  },
  {
    name: 'full',
    testMatch: /.*\.spec\.ts$/,
    testIgnore: /.*\.smoke\.spec\.ts$/,  // Loại trừ smoke để không chạy 2 lần
  },
],

Cả hai cách đều hợp lệ. Naming suffix dễ thêm vào file hiện tại mà không cần di chuyển. Subfolder rõ ràng hơn về ranh giới scope.

Chạy trong CI:

# Sau mỗi deploy — chỉ smoke
npx playwright test --project=smoke

# Nightly full suite
npx playwright test --project=full
9

Combine testMatch Và testIgnore

Khi dùng cùng nhau trong một project: Playwright tính giao — file phải khớp testMatch VÀ không khớp testIgnore. Logic là: testMatch(file) AND NOT testIgnore(file).

projects: [
  {
    name: 'main',
    // Include: mọi .spec.ts
    testMatch: /.*\.spec\.ts$/,
    // Exclude: .setup.ts và .teardown.ts (dù chúng cũng kết thúc bằng .ts)
    testIgnore: [/.*\.setup\.ts$/, /.*\.teardown\.ts$/],
  },
],

Ví dụ file list và kết quả:

auth.spec.ts          → match testMatch, không match testIgnore → CHẠY
checkout.spec.ts      → match testMatch, không match testIgnore → CHẠY
auth.setup.ts         → KHÔNG match testMatch (/.*\.spec\.ts$/) → KHÔNG CHẠY
auth.teardown.ts      → KHÔNG match testMatch                   → KHÔNG CHẠY
data.setup.ts         → KHÔNG match testMatch                   → KHÔNG CHẠY

Trường hợp cần testIgnore khi testMatch rộng hơn:

projects: [
  {
    name: 'all-except-slow',
    testMatch: '**/*.spec.ts',      // match tất cả
    testIgnore: '**/slow/**',        // trừ file trong folder slow/
  },
  {
    name: 'slow-only',
    testMatch: '**/slow/**/*.spec.ts',
  },
],

Khi testIgnore nhận array, file bị loại trừ nếu khớp bất kỳ phần tử nào (OR logic).

10

testMatch vs --grep: Khác Nhau Ở Đâu

Hai cơ chế filter hoàn toàn khác nhau về scope:

Tiêu chí testMatch / testIgnore --grep
Match trên Đường dẫn file Tên test (string truyền vào test('name', ...))
Scope File-level Test-level
Khai báo ở đâu Config (persistent) CLI (ad-hoc)
Ảnh hưởng load File không match không được load File vẫn được load, chỉ skip test không khớp
Dùng khi Tách loại test theo file (cố định trong config) Filter tạm thời để debug/chạy nhanh subset

Ví dụ phân biệt:

# testMatch: chỉ project unit mới load file *.unit.spec.ts
npx playwright test --project=unit

# --grep: load tất cả file, chỉ chạy test có "should validate" trong tên
npx playwright test --grep "should validate"

# Kết hợp: load file unit rồi filter theo tên
npx playwright test --project=unit --grep "should validate"

Một điểm khác biệt về hiệu năng: file không khớp testMatch không được parse và load vào Node.js — tiết kiệm thời gian đáng kể với codebase lớn. --grep vẫn load tất cả file trước rồi skip.

11

1 File Chạy Nhiều Project

Một file có thể khớp testMatch của nhiều project cùng lúc. Playwright sẽ chạy file đó một lần cho mỗi project — mỗi lần là một copy độc lập với context riêng.

projects: [
  {
    name: 'chromium',
    testMatch: '**/*.e2e.spec.ts',
    use: { ...devices['Desktop Chrome'] },
  },
  {
    name: 'firefox',
    testMatch: '**/*.e2e.spec.ts',   // Cùng pattern → cùng file
    use: { ...devices['Desktop Firefox'] },
  },
  {
    name: 'webkit',
    testMatch: '**/*.e2e.spec.ts',
    use: { ...devices['Desktop Safari'] },
  },
],

File checkout.e2e.spec.ts sẽ được chạy 3 lần. Trong HTML report, mỗi lần chạy hiển thị như một test riêng với tên project kèm theo.

Đây là hành vi mong muốn cho cross-browser testing. Tuy nhiên nếu không cần thiết (file chỉ dành cho 1 browser) mà vẫn để testMatch chồng nhau, file sẽ chạy dư gây lãng phí thời gian. Cần chú ý khi thiết kế pattern để tránh overlap không chủ ý.

12

Pitfall Thường Gặp

Pitfall 1 — Quên escape dấu chấm trong regex, match nhầm file

Trong regex, . là wildcard khớp bất kỳ ký tự nào. Để match ký tự chấm literal, phải dùng \.:

// ❌ SAI — dấu chấm là wildcard
// Pattern này sẽ khớp cả "authXsetupYts" (nếu tên file chứa ký tự thay thế)
testMatch: /.*\.unit.spec.ts$/

// ✅ ĐÚNG — escape dấu chấm
testMatch: /.*\.unit\.spec\.ts$/

Lỗi này hiếm xảy ra thực tế vì tên file thường không có ký tự lạ, nhưng khi debug pattern phức tạp hơn (ví dụ chứa dấu chấm trong folder name), đây là nguồn gốc của bug khó phát hiện.

Pitfall 2 — Nhầm glob * (single segment) với ** (recursive)

Glob * chỉ match tên file hoặc folder ở một segment — không đệ quy. Glob ** match qua nhiều segment:

// ❌ SAI — '*.spec.ts' chỉ match file ở root của testDir
// Không match tests/auth/login.spec.ts (có subfolder auth)
testMatch: '*.spec.ts'

// ✅ ĐÚNG — '**/*.spec.ts' match đệ quy
testMatch: '**/*.spec.ts'
tests/
  login.spec.ts          → khớp '*.spec.ts' ✅ và '**/*.spec.ts' ✅
  auth/
    login.spec.ts        → KHÔNG khớp '*.spec.ts' ❌  khớp '**/*.spec.ts' ✅
    session.spec.ts      → KHÔNG khớp '*.spec.ts' ❌  khớp '**/*.spec.ts' ✅

Pitfall 3 — testMatch overlap khiến file chạy nhiều lần không mong muốn

Khi 2 project có testMatch overlap — cùng khớp một file — file đó chạy 2 lần trong 2 project. Nếu không phải cross-browser setup có chủ ý, đây là lãng phí và có thể gây hiểu nhầm kết quả:

// ❌ Overlap: auth.spec.ts khớp cả 2 project
projects: [
  {
    name: 'auth',
    testMatch: '**/*.spec.ts',  // quá rộng
  },
  {
    name: 'api',
    testMatch: '**/*.spec.ts',  // cùng pattern → mọi file chạy 2 lần
  },
]

// ✅ Tách rõ ràng
projects: [
  {
    name: 'auth',
    testDir: 'tests/auth',
  },
  {
    name: 'api',
    testDir: 'tests/api',
  },
]

Pitfall 4 — Override testMatch project nhưng quên testIgnore, file setup chạy như test thường

Khi project dùng pattern rộng (**/*.ts hoặc **/*.spec.ts) mà trong codebase có file .setup.ts, các file này sẽ được load và chạy như test thông thường — không có setup/teardown lifecycle:

// ❌ SAI — auth.setup.ts sẽ chạy trong project 'main' nếu pattern rộng
{
  name: 'main',
  testMatch: '**/*.ts',  // quá rộng — bắt cả setup
}

// ✅ ĐÚNG — explicit exclude
{
  name: 'main',
  testMatch: '**/*.spec.ts',
  testIgnore: [
    '**/*.setup.ts',
    '**/*.teardown.ts',
    '**/helpers/**',   // loại trừ utility file nếu có extension .ts
  ],
}

Quy tắc chung: càng dùng pattern testMatch rộng, càng cần testIgnore explicit để loại trừ file không phải test.

13

Quiz

Câu 1. Config sau có bao nhiêu lần file login.e2e.spec.ts được chạy?

projects: [
  { name: 'chromium', testMatch: /.*\.e2e\.spec\.ts$/, use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  testMatch: /.*\.e2e\.spec\.ts$/, use: { ...devices['Desktop Firefox'] } },
  { name: 'unit',     testMatch: /.*\.unit\.spec\.ts$/ },
]
Đáp án

2 lần — một lần trong project chromium và một lần trong project firefox. Project unit không khớp vì pattern khác. Mỗi lần chạy là một copy độc lập với browser và context riêng.

Câu 2. Glob pattern 'tests/api/*.spec.ts' có match file tests/api/v2/user.spec.ts không?

Đáp án

Không. Glob * chỉ match 1 segment path, không đệ quy qua v2/. Phải dùng 'tests/api/**/*.spec.ts' để match file trong subfolder bất kỳ.

Câu 3. Project maintestMatch: /.*\.spec\.ts$/testIgnore: /.*\.setup\.ts$/. File auth.setup.ts có chạy trong project này không?

Đáp án

Không. Dù file kết thúc .ts chứ không phải .spec.ts nên không khớp testMatch — đã bị loại ở bước đầu. Kể cả nếu testMatch rộng hơn và khớp file, testIgnore sẽ loại trừ nó.

Câu 4. Regex /.*\.unit.spec\.ts$/ (thiếu escape ở dấu chấm thứ hai) có thể khớp file nào không mong muốn?

Đáp án

Có thể khớp file như auth.unitXspecYts (dấu chấm không escape là wildcard khớp bất kỳ ký tự). Trong thực tế tên file thường dùng ký tự chữ/số nên ít xảy ra, nhưng đây vẫn là bug tiềm ẩn. Pattern đúng là /.*\.unit\.spec\.ts$/.

Câu 5. Top-level config khai báo testMatch: '**/*.spec.ts'. Project special khai báo testMatch: 'special/*.ts'. File special/helper.ts có chạy trong project special không?

Đáp án

Có. Project special khai báo testMatch riêng nên top-level pattern '**/*.spec.ts' bị bỏ qua hoàn toàn cho project này. Pattern 'special/*.ts' match special/helper.ts (glob *.ts khớp bất kỳ file .ts nào ngay trong folder special). Playwright sẽ load và chạy file đó như một test file.