#Data-Driven Testing with Playwright

การทดสอบระบบจริงมักไม่ได้มีเพียงข้อมูลชุดเดียว เช่น หน้า Login อาจต้องทดสอบทั้งผู้ใช้ที่ถูกต้อง รหัสผ่านผิด ช่องว่าง หรือข้อมูลหลายรูปแบบ หากเราเขียน Test Script แยกทีละกรณี โค้ดจะซ้ำและดูแลรักษายาก

Data-Driven Testing (DDT) คือแนวทางที่แยก Test Data ออกจาก Test Logic แล้วนำข้อมูลหลายชุดมารันกับขั้นตอนการทดสอบเดียวกัน

Playwright Test รองรับแนวทางนี้ได้โดยตรงผ่านการสร้าง Parameterized Tests และสามารถอ่านข้อมูลจาก Array, JSON, CSV หรือแหล่งข้อมูลอื่นผ่าน Node.js ได้


#1. Data-Driven Testing คืออะไร

แนวคิดหลักคือ

Test Data
   │
   ├── Case 1
   ├── Case 2
   ├── Case 3
   └── Case N
        │
        ▼
Reusable Test Logic
        │
        ▼
Playwright Test Runner
        │
        ▼
Pass / Fail / Report

แทนที่จะเขียน

test('login user A', async ({ page }) => {
  // ...
});

test('login user B', async ({ page }) => {
  // ...
});

test('login user C', async ({ page }) => {
  // ...
});

เราสามารถเก็บข้อมูลเป็นชุด

const testData = [
  { username: 'user-a', password: 'pass-a' },
  { username: 'user-b', password: 'pass-b' },
  { username: 'user-c', password: 'pass-c' },
];

จากนั้นสร้าง Test Case จากข้อมูลแต่ละรายการ

for (const data of testData) {
  test(`login with ${data.username}`, async ({ page }) => {
    // ใช้ Test Logic ชุดเดียวกัน
  });
}

ข้อดีคือ

  • ลดโค้ดซ้ำ
  • เพิ่ม Test Case ได้ง่าย
  • แยก Test Data ออกจาก Test Logic
  • รองรับข้อมูลจำนวนมาก
  • เหมาะกับ Regression Test
  • เหมาะกับ Boundary Value และ Equivalence Partitioning
  • รายงานผลแยกตามข้อมูลแต่ละชุดได้
  • ใช้ร่วมกับ CI/CD ได้ง่าย

#2. เตรียมโปรเจกต์ Playwright

#2.1 สิ่งที่ควรมี

Playwright รุ่นปัจจุบันใช้ Node.js รุ่นที่รองรับตามเอกสารของ Playwright

ตรวจสอบ Node.js

node --version
npm --version

เริ่มโปรเจกต์ Playwright

npm init playwright@latest

ระหว่างติดตั้งสามารถเลือก

TypeScript
tests
GitHub Actions
Install Playwright browsers

หากมีโปรเจกต์ Node.js อยู่แล้ว สามารถติดตั้งเพิ่มได้ด้วย

npm install -D @playwright/test@latest
npx playwright install

ตรวจสอบเวอร์ชัน

npx playwright --version

#3. โครงสร้างโปรเจกต์ตัวอย่าง

playwright-data-driven/
├── data/
│   ├── login-data.json
│   └── login-data.csv
├── pages/
│   └── LoginPage.ts
├── tests/
│   ├── login-array.spec.ts
│   ├── login-json.spec.ts
│   ├── login-csv.spec.ts
│   └── login-pom.spec.ts
├── playwright.config.ts
├── package.json
└── tsconfig.json

แนวคิดการแบ่งความรับผิดชอบ

data/
  เก็บ Test Data

pages/
  เก็บ Page Object

tests/
  เก็บ Test Scenario และ Assertions

playwright.config.ts
  เก็บ Configuration

#4. เว็บไซต์สำหรับทดลอง

ตัวอย่างนี้ใช้

https://seleniumbase.io/simple/login

ข้อมูลเข้าสู่ระบบตัวอย่างคือ

Username: demo_user
Password: secret_pass

หน้าเว็บมี element หลักที่สามารถใช้ได้ เช่น

#username
#password
Sign in

#5. ตัวอย่างที่ 1: Data-Driven Testing ด้วย Array

สร้างไฟล์

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

type LoginData = {
  caseName: string;
  username: string;
  password: string;
  shouldLogin: boolean;
};

const loginData: LoginData[] = [
  {
    caseName: 'valid username and password',
    username: 'demo_user',
    password: 'secret_pass',
    shouldLogin: true,
  },
  {
    caseName: 'invalid password',
    username: 'demo_user',
    password: 'wrong_password',
    shouldLogin: false,
  },
  {
    caseName: 'invalid username',
    username: 'wrong_user',
    password: 'secret_pass',
    shouldLogin: false,
  },
  {
    caseName: 'empty username and password',
    username: '',
    password: '',
    shouldLogin: false,
  },
];

for (const data of loginData) {
  test(`Login: ${data.caseName}`, async ({ page }) => {
    await page.goto('https://seleniumbase.io/simple/login');

    await page.locator('#username').fill(data.username);
    await page.locator('#password').fill(data.password);

    await page.getByRole('link', { name: 'Sign in' }).click();

    if (data.shouldLogin) {
      await expect(
        page.getByRole('heading', { name: 'Welcome!' })
      ).toBeVisible();
    } else {
      await expect(
        page.getByRole('heading', { name: 'Welcome!' })
      ).not.toBeVisible();
    }
  });
}

รัน

npx playwright test tests/login-array.spec.ts

รันแบบเห็น Browser

npx playwright test tests/login-array.spec.ts --headed

เปิด UI Mode

npx playwright test --ui

#6. ทำไมชื่อ Test Case ต้องไม่ซ้ำกัน

ชื่อ Test ควรบอกว่าข้อมูลชุดใดกำลังถูกทดสอบ

ตัวอย่างที่ดี

test(`Login: ${data.caseName}`, async ({ page }) => {
  // ...
});

ผลใน Report จะอ่านง่าย เช่น

✓ Login: valid username and password
✓ Login: invalid password
✓ Login: invalid username
✓ Login: empty username and password

หากใช้เพียง

test('login test', ...)

กับข้อมูลทุกชุด การวิเคราะห์ผลจะทำได้ยากกว่า


#7. ตัวอย่างที่ 2: ใช้ test.describe() แยกกลุ่มข้อมูล

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

const users = [
  {
    name: 'valid-user',
    username: 'demo_user',
    password: 'secret_pass',
    expected: true,
  },
  {
    name: 'wrong-password',
    username: 'demo_user',
    password: '123456',
    expected: false,
  },
];

for (const user of users) {
  test.describe(`Login dataset: ${user.name}`, () => {

    test.beforeEach(async ({ page }) => {
      await page.goto('https://seleniumbase.io/simple/login');
    });

    test('should return expected login result', async ({ page }) => {
      await page.locator('#username').fill(user.username);
      await page.locator('#password').fill(user.password);
      await page.getByRole('link', { name: 'Sign in' }).click();

      const welcome = page.getByRole('heading', {
        name: 'Welcome!',
      });

      if (user.expected) {
        await expect(welcome).toBeVisible();
      } else {
        await expect(welcome).not.toBeVisible();
      }
    });
  });
}

รูปแบบนี้เหมาะเมื่อต้องการ

  • Hooks แยกตาม Dataset
  • กลุ่ม Test ที่อ่านง่าย
  • Setup/Teardown เฉพาะชุดข้อมูล

#8. ตัวอย่างที่ 3: Data-Driven Testing จาก JSON

เมื่อ Test Data มากขึ้น ควรแยกออกจาก Test Script

สร้าง

data/login-data.json
[
  {
    "caseName": "valid login",
    "username": "demo_user",
    "password": "secret_pass",
    "shouldLogin": true
  },
  {
    "caseName": "wrong password",
    "username": "demo_user",
    "password": "wrong_password",
    "shouldLogin": false
  },
  {
    "caseName": "wrong username",
    "username": "guest_user",
    "password": "secret_pass",
    "shouldLogin": false
  },
  {
    "caseName": "empty credentials",
    "username": "",
    "password": "",
    "shouldLogin": false
  }
]

สร้าง

tests/login-json.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';

type LoginData = {
  caseName: string;
  username: string;
  password: string;
  shouldLogin: boolean;
};

const filePath = path.resolve(
  process.cwd(),
  'data/login-data.json'
);

const loginData: LoginData[] = JSON.parse(
  fs.readFileSync(filePath, 'utf-8')
);

for (const data of loginData) {
  test(`JSON - ${data.caseName}`, async ({ page }) => {
    await page.goto('https://seleniumbase.io/simple/login');

    await page.locator('#username').fill(data.username);
    await page.locator('#password').fill(data.password);
    await page.getByRole('link', { name: 'Sign in' }).click();

    const welcome = page.getByRole('heading', {
      name: 'Welcome!',
    });

    if (data.shouldLogin) {
      await expect(welcome).toBeVisible();
    } else {
      await expect(welcome).not.toBeVisible();
    }
  });
}

รัน

npx playwright test tests/login-json.spec.ts

ข้อดีของ JSON คือ

  • อ่านง่าย
  • รองรับ object ซ้อนกัน
  • เหมาะกับ API/E2E Test
  • ใช้ร่วมกับ JavaScript/TypeScript ได้ดี
  • ไม่ต้องติดตั้ง Parser เพิ่ม

#9. ตัวอย่างที่ 4: Data-Driven Testing จาก CSV

CSV เหมาะเมื่อ Test Data ถูกจัดเตรียมโดย Tester, BA หรือผู้ใช้ที่ทำงานกับ Spreadsheet

ติดตั้ง Parser

npm install -D csv-parse

สร้าง

data/login-data.csv
caseName,username,password,shouldLogin
valid login,demo_user,secret_pass,true
wrong password,demo_user,wrong_password,false
wrong username,guest_user,secret_pass,false
empty credentials,,,false

สร้าง

tests/login-csv.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { parse } from 'csv-parse/sync';

type CsvLoginData = {
  caseName: string;
  username: string;
  password: string;
  shouldLogin: string;
};

const filePath = path.resolve(
  process.cwd(),
  'data/login-data.csv'
);

const records = parse(
  fs.readFileSync(filePath),
  {
    columns: true,
    skip_empty_lines: true,
    trim: true,
  }
) as CsvLoginData[];

for (const data of records) {
  test(`CSV - ${data.caseName}`, async ({ page }) => {
    await page.goto('https://seleniumbase.io/simple/login');

    await page.locator('#username').fill(data.username ?? '');
    await page.locator('#password').fill(data.password ?? '');
    await page.getByRole('link', { name: 'Sign in' }).click();

    const welcome = page.getByRole('heading', {
      name: 'Welcome!',
    });

    const shouldLogin =
      data.shouldLogin.toLowerCase() === 'true';

    if (shouldLogin) {
      await expect(welcome).toBeVisible();
    } else {
      await expect(welcome).not.toBeVisible();
    }
  });
}

รัน

npx playwright test tests/login-csv.spec.ts

Playwright ทำงานบน Node.js จึงสามารถอ่านไฟล์ผ่าน File System และใช้ CSV library ของ npm ได้โดยตรง


#10. Array, JSON หรือ CSV ควรเลือกอะไร

รูปแบบ เหมาะกับ จุดเด่น ข้อควรระวัง
Array ข้อมูลน้อย เขียนง่ายที่สุด Test Script โตเร็ว
JSON ข้อมูลเชิงโครงสร้าง Type-friendly, อ่านง่าย ผู้ใช้ธุรกิจอาจแก้ไขไม่สะดวก
CSV ตารางข้อมูลจำนวนมาก เปิดด้วย Spreadsheet ได้ Data Type เป็น string เป็นหลัก
Database Integration/Enterprise ใช้ข้อมูลจริงหรือ seeded data ต้องควบคุม state
API Dynamic test data ทดสอบระบบแบบ end-to-end เพิ่ม dependency ภายนอก

สำหรับโปรเจกต์ทั่วไปสามารถเริ่มจาก

Array → JSON → CSV/Database

ตามขนาดและความซับซ้อนของข้อมูล


#11. ใช้ Page Object Model ร่วมกับ Data-Driven Testing

เมื่อ Test Script เริ่มใหญ่ ไม่ควรให้ selector และ action กระจายอยู่ในทุก Test File

สร้าง

pages/LoginPage.ts
import {
  Page,
  Locator,
  expect,
} from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly username: Locator;
  readonly password: Locator;
  readonly signIn: Locator;
  readonly welcomeHeading: Locator;

  constructor(page: Page) {
    this.page = page;
    this.username = page.locator('#username');
    this.password = page.locator('#password');
    this.signIn = page.getByRole('link', {
      name: 'Sign in',
    });
    this.welcomeHeading = page.getByRole(
      'heading',
      { name: 'Welcome!' }
    );
  }

  async goto() {
    await this.page.goto(
      'https://seleniumbase.io/simple/login'
    );
  }

  async login(username: string, password: string) {
    await this.username.fill(username);
    await this.password.fill(password);
    await this.signIn.click();
  }

  async expectLoginSuccess() {
    await expect(this.welcomeHeading).toBeVisible();
  }

  async expectLoginFailure() {
    await expect(this.welcomeHeading).not.toBeVisible();
  }
}

จากนั้นสร้าง

tests/login-pom.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

const cases = [
  {
    name: 'valid credentials',
    username: 'demo_user',
    password: 'secret_pass',
    expected: true,
  },
  {
    name: 'invalid password',
    username: 'demo_user',
    password: 'invalid',
    expected: false,
  },
];

for (const data of cases) {
  test(`POM - ${data.name}`, async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();

    await loginPage.login(
      data.username,
      data.password
    );

    if (data.expected) {
      await loginPage.expectLoginSuccess();
    } else {
      await loginPage.expectLoginFailure();
    }
  });
}

จะเห็นว่า Test Case อ่านง่ายขึ้นเป็น

Prepare Data
→ Open Page
→ Login
→ Verify Expected Result

#12. แยก Data และ Logic อย่างเป็นระบบ

โครงสร้างที่แนะนำ

                 ┌──────────────────┐
                 │   Test Dataset   │
                 │ JSON / CSV / DB  │
                 └────────┬─────────┘
                          │
                          ▼
                 ┌──────────────────┐
                 │  Playwright Test │
                 │   Parameterize   │
                 └────────┬─────────┘
                          │
                  ┌───────┴────────┐
                  ▼                ▼
          ┌──────────────┐  ┌──────────────┐
          │ Page Objects │  │   Fixtures   │
          └──────┬───────┘  └──────┬───────┘
                 └────────┬─────────┘
                          ▼
                 ┌──────────────────┐
                 │ Browser / System │
                 │   Under Test     │
                 └────────┬─────────┘
                          ▼
                 ┌──────────────────┐
                 │ Assert + Report  │
                 └──────────────────┘

#13. ใช้ Fixtures เมื่อข้อมูลเป็น Test Context

Data-Driven Testing ไม่ได้หมายความว่าข้อมูลทุกอย่างต้องมาจาก CSV/JSON

ข้อมูลที่เป็น Configuration หรือ Context เช่น

  • role
  • locale
  • tenant
  • environment
  • default user
  • API client
  • authenticated session

เหมาะกับ Playwright Fixtures

ตัวอย่าง

import { test as base } from '@playwright/test';

type TestOptions = {
  role: string;
};

export const test = base.extend<TestOptions>({
  role: ['student', { option: true }],
});

จากนั้นใช้ใน Test

test('show dashboard by role', async ({
  page,
  role,
}) => {
  console.log(role);
});

#14. Parameterized Projects

หากต้องการรัน Test ชุดเดียวกับ Configuration หลายแบบ สามารถใช้ projects

ตัวอย่าง

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

export default defineConfig({
  projects: [
    {
      name: 'student',
      use: {
        locale: 'th-TH',
      },
    },
    {
      name: 'international',
      use: {
        locale: 'en-US',
      },
    },
  ],
});

เหมาะกับ

Browser
Environment
Locale
Device
Role
Tenant
Configuration

แต่ถ้าเป็น

username
password
product
quantity
search keyword
form input

ควรใช้ Dataset เพื่อสร้าง Parameterized Tests มากกว่า


#15. Data-Driven Testing กับ Boundary Value Analysis

DDT เหมาะมากกับการทดสอบขอบเขต

สมมติระบบรับอายุ

18 - 60 ปี

Dataset

const ageCases = [
  { age: 17, valid: false },
  { age: 18, valid: true },
  { age: 19, valid: true },
  { age: 59, valid: true },
  { age: 60, valid: true },
  { age: 61, valid: false },
];

Test

for (const data of ageCases) {
  test(`age=${data.age}`, async ({ page }) => {
    // กรอก age
    // submit
    // verify ตาม data.valid
  });
}

นี่ทำให้ Test Design และ Automation เชื่อมกันโดยตรง


#16. Data-Driven Testing กับ Equivalence Partitioning

ตัวอย่าง Username

Valid:
- ตัวอักษร 6-20 ตัว

Invalid:
- น้อยกว่า 6
- มากกว่า 20
- ตัวอักษรต้องห้าม
- ค่าว่าง

Dataset

const usernameCases = [
  {
    value: 'tester01',
    expected: true,
  },
  {
    value: 'abc',
    expected: false,
  },
  {
    value: '',
    expected: false,
  },
  {
    value: 'user name',
    expected: false,
  },
];

ทำให้ 1 Test Logic สามารถครอบคลุมหลาย Partition ได้


#17. อย่าใส่ Password จริงไว้ใน Test Data

ตัวอย่างที่ไม่ควรทำ

{
  "username": "admin",
  "password": "ProductionPassword123!"
}

เพราะข้อมูลอาจถูก commit เข้า Git

ควรใช้ Environment Variables หรือ Secret Manager

ตัวอย่าง

const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;

บน macOS/Linux

TEST_USERNAME=demo_user \
TEST_PASSWORD=secret_pass \
npx playwright test

PowerShell

$env:TEST_USERNAME="demo_user"
$env:TEST_PASSWORD="secret_pass"

npx playwright test

ใน GitHub Actions ควรเก็บใน

GitHub Secrets

แล้วส่งเป็น environment variable


#18. Configuration ที่เหมาะกับ Test Automation

ตัวอย่าง playwright.config.ts

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

export default defineConfig({
  testDir: './tests',

  fullyParallel: true,

  forbidOnly: !!process.env.CI,

  retries: process.env.CI ? 2 : 0,

  workers: process.env.CI ? 1 : undefined,

  reporter: [
    ['list'],
    ['html', { open: 'never' }],
  ],

  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
      },
    },
  ],
});

ข้อดีคือเมื่อ Data-Driven Test มีหลายกรณี หากบางรายการล้มเหลวจะมีหลักฐานช่วยวิเคราะห์ เช่น

  • Screenshot
  • Video
  • Trace
  • HTML Report

#19. รัน Test แบบต่าง ๆ

รันทั้งหมด

npx playwright test

รันไฟล์เดียว

npx playwright test tests/login-json.spec.ts

รัน Chromium

npx playwright test --project=chromium

รันแบบ Headed

npx playwright test --headed

เปิด UI Mode

npx playwright test --ui

Debug

npx playwright test --debug

เปิด HTML Report

npx playwright show-report

#20. ใช้ Tag/Filter กับ Data-Driven Test

สามารถตั้งชื่อหรือ Tag ให้ Test Case เพื่อเลือกรันเฉพาะกลุ่มได้

ตัวอย่างแนวคิด Dataset

const cases = [
  {
    name: 'valid login',
    tag: '@smoke',
    username: 'demo_user',
    password: 'secret_pass',
  },
  {
    name: 'wrong password',
    tag: '@regression',
    username: 'demo_user',
    password: 'wrong',
  },
];

สร้างชื่อ Test

for (const data of cases) {
  test(`${data.tag} ${data.name}`, async ({ page }) => {
    // ...
  });
}

รันเฉพาะ Smoke Test

npx playwright test --grep @smoke

#21. GitHub Actions สำหรับ Data-Driven Playwright Test

ตัวอย่าง

.github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

Pipeline

Push / Pull Request
        │
        ▼
Install Dependencies
        │
        ▼
Install Browsers
        │
        ▼
Load Test Data
        │
        ▼
Run Parameterized Tests
        │
        ▼
Generate HTML Report
        │
        ▼
Upload Artifact

#22. Parallel Execution กับ Data-Driven Testing

Playwright รองรับการรัน Test แบบ Parallel

เมื่อเราเขียน

for (const data of loginData) {
  test(`Login ${data.caseName}`, async ({ page }) => {
    // ...
  });
}

ข้อมูลแต่ละรายการถูกสร้างเป็น Test Case แยกกัน จึงสามารถใช้ความสามารถของ Playwright Test Runner ในการจัดสรรงานให้ worker ได้ตาม configuration

อย่างไรก็ตาม ต้องระวังเมื่อ Test Data ใช้ Resource ร่วมกัน เช่น

บัญชีเดียวกัน
ข้อมูล Database record เดียวกัน
Order เดียวกัน
ไฟล์เดียวกัน
Shared state

แนวทางแก้ไข

ใช้ข้อมูลที่ unique
สร้างข้อมูลก่อน Test
ลบข้อมูลหลัง Test
ใช้ Fixture
ใช้ API สำหรับ seed data
แยก Database/Test Environment

#23. การออกแบบ Dataset ที่ดี

Dataset ควรมีอย่างน้อย

caseName
input
expected

ตัวอย่าง

{
  "caseName": "Login with valid credentials",
  "username": "demo_user",
  "password": "secret_pass",
  "expected": {
    "loggedIn": true,
    "heading": "Welcome!"
  }
}

ข้อดีคือ Test Data อธิบาย Expected Result ได้ชัดเจน


#24. หลีกเลี่ยง Test Logic ใน Dataset

ไม่ควรเขียน Dataset แบบ

{
  "action": "click",
  "selector": "#login",
  "wait": 3000,
  "assert": "h1"
}

เพราะจะกลายเป็นการสร้าง Testing Framework ซ้อน Testing Framework และทำให้ Debug ยาก

Dataset ควรบอก

Input
Expected Result
Business Condition

ส่วน

Click
Fill
Navigation
Assertion implementation

ควรอยู่ใน Test Logic หรือ Page Object


#25. Best Practices

#25.1 ตั้งชื่อ Test Case ให้ระบุ Dataset

ดี

test(`Login - ${data.caseName}`, ...)

ไม่แนะนำ

test('login test', ...)

#25.2 ใช้ TypeScript Type

type LoginData = {
  caseName: string;
  username: string;
  password: string;
  shouldLogin: boolean;
};

ช่วยลดความผิดพลาดจาก Test Data


#25.3 Validate External Test Data

หากอ่านจาก CSV/JSON จำนวนมาก ควรตรวจสอบก่อนสร้าง Test

if (!data.caseName) {
  throw new Error('caseName is required');
}

โปรเจกต์ขนาดใหญ่สามารถใช้ Schema Validator เช่น Zod


#25.4 อย่าใช้ index เป็นชื่อ Test เพียงอย่างเดียว

ไม่แนะนำ

case 1
case 2
case 3

แนะนำ

valid credentials
wrong password
empty username
username boundary 20 chars

#25.5 ทำให้แต่ละ Dataset Independent

Test Case ไม่ควรขึ้นกับผลของ Test ก่อนหน้า

ไม่ควรเป็น

Case 1 สร้าง User
Case 2 Login User จาก Case 1
Case 3 ลบ User จาก Case 1

ควรให้แต่ละ Test มี Setup ที่ชัดเจน หรือใช้ Fixture


#25.6 หลีกเลี่ยง Hard Wait

ไม่ควรใช้

await page.waitForTimeout(5000);

หากไม่ได้จำเป็นจริง

ควรใช้ Web-First Assertion

await expect(
  page.getByRole('heading', {
    name: 'Welcome!',
  })
).toBeVisible();

#26. Data-Driven Testing ไม่ใช่การ Loop ภายใน Test เดียว

รูปแบบที่ไม่แนะนำ

test('login all users', async ({ page }) => {
  for (const user of users) {
    // test user
  }
});

ปัญหาคือ

Dataset 1 PASS
Dataset 2 PASS
Dataset 3 FAIL
Dataset 4 ไม่ได้รัน

Report จะแสดงเป็น Test เดียว

ควรสร้าง Test แยกจาก Dataset

for (const user of users) {
  test(`login ${user.name}`, async ({ page }) => {
    // ...
  });
}

ผลคือ

PASS user 1
PASS user 2
FAIL user 3
PASS user 4

วิเคราะห์ผลได้ง่ายกว่ามาก


#27. Architecture สำหรับโปรเจกต์จริง

แนวทางที่เหมาะกับโปรเจกต์ขนาดกลางถึงใหญ่

tests/
│
├── data/
│   ├── users.json
│   ├── products.json
│   └── orders.csv
│
├── fixtures/
│   ├── auth.fixture.ts
│   └── api.fixture.ts
│
├── pages/
│   ├── LoginPage.ts
│   ├── ProductPage.ts
│   └── CheckoutPage.ts
│
├── specs/
│   ├── login.spec.ts
│   ├── product.spec.ts
│   └── checkout.spec.ts
│
└── utils/
    ├── csv.ts
    ├── data-generator.ts
    └── validation.ts

แนวคิดคือ

Data
  ↓
Fixtures / Data Loader
  ↓
Test Scenario
  ↓
Page Object / API Client
  ↓
System Under Test
  ↓
Assertions
  ↓
Report

#28. ตัวอย่าง Data Loader

สร้าง

utils/data-loader.ts
import fs from 'node:fs';
import path from 'node:path';

export function loadJson<T>(
  relativePath: string
): T {
  const filePath = path.resolve(
    process.cwd(),
    relativePath
  );

  return JSON.parse(
    fs.readFileSync(filePath, 'utf-8')
  ) as T;
}

ใช้งาน

import { loadJson } from '../utils/data-loader';

type LoginData = {
  caseName: string;
  username: string;
  password: string;
  shouldLogin: boolean;
};

const users = loadJson<LoginData[]>(
  'data/login-data.json'
);

ช่วยลดการเขียน File I/O ซ้ำหลาย Test File


#29. เมื่อใดควรใช้ Data-Driven Testing

เหมาะกับกรณี

#Login

Valid user
Invalid user
Locked user
Inactive user
Different roles

#Form Validation

Required field
Min length
Max length
Invalid format
Boundary values

#Search

Keyword
Empty query
Special characters
Unicode
Thai/English

#E-Commerce

Product
Quantity
Coupon
Shipping method
Payment method

#API Testing

Request body
Status code
Expected response
Validation rules

#Role-Based Access Control

Admin
Teacher
Student
Guest

#30. เมื่อใดไม่ควรใช้ Data-Driven Testing

ไม่จำเป็นต้องใช้ DDT หาก

  • มีข้อมูลเพียง 1 ชุด
  • Scenario แต่ละชุดมี Flow แตกต่างกันมาก
  • Test Data มี dependency ซับซ้อน
  • Test Logic ต้องเปลี่ยนตามข้อมูลแทบทั้งหมด
  • การอ่าน Dataset ทำให้ Test เข้าใจยากกว่าการเขียนแยก

หลักการคือ

ใช้ Data-Driven Testing เมื่อ “ขั้นตอนเหมือนเดิม แต่ข้อมูลและ Expected Result เปลี่ยน”


#31. สรุป

Data-Driven Testing ใน Playwright สามารถเริ่มได้ง่ายจาก Parameterized Tests

Test Data
   ↓
for / forEach
   ↓
Playwright Test
   ↓
Page Object / Fixtures
   ↓
Assertions
   ↓
HTML Report / CI

แนวทางเลือกใช้

ข้อมูลน้อย
→ Array

ข้อมูลแยกเป็นไฟล์และมีโครงสร้าง
→ JSON

ข้อมูลตารางจำนวนมาก
→ CSV

Configuration หลายชุด
→ Projects / Option Fixtures

Setup และ Context ที่ reusable
→ Fixtures

โปรเจกต์ขนาดกลาง-ใหญ่
→ Data + Page Object + Fixtures + CI

จุดสำคัญที่สุดคือ สร้าง Test Case แยกสำหรับข้อมูลแต่ละชุด แทนการวน Loop ทุก Dataset ภายใน test() เดียว เพื่อให้ Playwright สามารถรายงานผล, retry, parallelize และ debug แต่ละกรณีได้อย่างชัดเจน


#32. คำสั่งสรุปสำหรับ Workshop

# สร้างโปรเจกต์
npm init playwright@latest

# เข้าโฟลเดอร์
cd playwright-data-driven

# ติดตั้ง CSV parser
npm install -D csv-parse

# รัน Test ทั้งหมด
npx playwright test

# รันแบบเห็น Browser
npx playwright test --headed

# เปิด UI Mode
npx playwright test --ui

# Debug
npx playwright test --debug

# เปิด HTML Report
npx playwright show-report

#References