Chương 13 Trung Cấp
Test Automation
Từ automation strategy đến Playwright implementation, Page Object Model và CI/CD integration — con đường lên Senior QA.
🎯 Automation Strategy Required
| Nên Automate ✅ | Không nên Automate ❌ |
|---|---|
| Regression tests chạy mỗi sprint | Test chỉ chạy 1 lần (one-time) |
| Smoke tests sau mỗi deploy | Exploratory và usability testing |
| Data-driven tests (nhiều bộ data) | UI thay đổi rất thường xuyên |
| Performance / load testing | New features đang development |
| API contract testing | Tính năng không stable |
| Cross-browser compatibility | Khi cost maintain > value gained |
Test Pyramid — Kim tự tháp kiểm thử:
- Unit Tests (70%): Dev viết, chạy nhanh (<1ms/test), hàng nghìn tests. Feedback ngay lập tức.
- Integration/API Tests (20%): QA + Dev, chạy vừa (<1s/test), test tích hợp giữa services.
- E2E UI Tests (10%): QA, chạy chậm (5-30s/test), chỉ critical user flows. Ít nhưng có giá trị cao.
⚠️ Ice Cream Cone Anti-pattern
Nhiều E2E tests (chậm, flaky, đắt) + ít unit tests = "Ice Cream Cone." Ngược với Test Pyramid. Kết quả: CI chậm, nhiều false failures, team mất niềm tin vào tests. Recognize và fix pattern này.
🎭 Playwright — Hướng Dẫn Từng Bước
# 1. Setup project
npm init playwright@latest
# Chọn: TypeScript, tests/ folder, GitHub Actions
# 2. First test file: tests/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test('successful login redirects to dashboard', async ({ page }) => {
await page.goto('/login');
// Fill form using semantic locators (preferred)
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('Password123!');
await page.getByRole('button', { name: 'Sign in' }).click();
// Assertions
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('wrong@test.com');
await page.getByLabel('Password').fill('wrongpass');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid email or password')).toBeVisible();
await expect(page).toHaveURL('/login'); // stayed on login
});
});
Locator Priority (best → worst):
getByRole('button', {name: 'Submit'})— Accessible role + name. Most resilient.getByLabel('Email address')— Form label. Good for inputs.getByText('Click here')— Visible text. Breaks if text changes.getByPlaceholder('Enter email')— Placeholder text.getByTestId('submit-btn')— data-testid attribute. Requires dev cooperation.locator('.btn-primary')— CSS selector. Last resort, fragile.locator('//button[1]')— XPath. Avoid unless necessary.
🏗️ Page Object Model (POM)
// pages/LoginPage.ts
import { Page, expect } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
// Locators as getters — lazy evaluated
get emailInput() { return this.page.getByLabel('Email'); }
get passwordInput() { return this.page.getByLabel('Password'); }
get signInButton() { return this.page.getByRole('button', {name: 'Sign in'}); }
get errorMessage() { return this.page.getByRole('alert'); }
// Action methods
async navigate() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.signInButton.click();
}
}
// tests/login.spec.ts — Clean test using POM
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login success', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@test.com', 'Password123!');
await expect(page).toHaveURL('/dashboard');
});
🚀 GitHub Actions CI Integration
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload test report
uses: actions/upload-artifact@v4
if: always() # upload even on failure
with:
name: playwright-report
path: playwright-report/
retention-days: 14
⚠️ Flaky Test Management
Nguyên nhân phổ biến và cách fix:
- Race conditionClick button trước khi load xong. Fix: dùng
await expect(element).toBeVisible()trước khi interact. Playwright auto-waits nhưng check custom conditions. - Hard-coded waits
await page.waitForTimeout(3000)— fragile. Fix: dùngawait page.waitForResponse('/api/data')hoặc wait for element state change. - Shared test dataTests dùng chung data, can thiệp nhau khi chạy parallel. Fix: mỗi test tạo data riêng, cleanup sau.
- Order dependencyTest B cần kết quả của Test A. Fix: tests phải independent. Dùng
beforeEachđể setup state. - Dynamic contentTimestamp, random IDs trong assertions. Fix: dùng regex hoặc check existence, không check exact value.
- Environment issuesTest pass locally, fail CI. Thường do: timezone, locale, network conditions. Fix: consistent Docker environment.
🏆 Senior Insight: Automation ROI
Senior QA không chỉ viết automation — họ biết tính ROI. Nếu 1 test case mất 2 giờ viết + 30 phút maintain mỗi sprint, nhưng manual testing chỉ mất 5 phút × 20 sprints = 100 phút = 1.7 giờ → automation KHÔNG có ROI ở đây. Focus automation vào high-frequency, time-consuming, repetitive tests.
⚖️ Framework Comparison
| Playwright | Cypress | Selenium WebDriver | |
|---|---|---|---|
| Language | JS/TS/Python/Java/C# | JavaScript/TypeScript | Java/Python/C#/Ruby/JS |
| Browsers | Chromium, Firefox, WebKit (Safari) | Chrome, Edge, Firefox | All major browsers |
| Speed | Fast (parallel native) | Fast but single tab | Slow (HTTP protocol overhead) |
| Setup | Very easy | Easy | Complex (WebDriver setup) |
| Auto-wait | Yes (smart) | Yes | No (manual waits) |
| Multi-tab | Yes | Limited | Yes |
| Best for | Modern teams, cross-browser | Frontend-focused teams | Legacy projects, Java shops |
| Job market | Growing fast | Popular | Large existing base |
✏️ Bài Tập
📝 Bài Tập: Page Object Model
Tạo POM và test cho Registration form có các fields: First Name, Last Name, Email, Password, Confirm Password, Agree to Terms checkbox, Submit button.
- Viết
RegistrationPage.tsvới tất cả locators và action methods - Viết 5 test cases: successful registration, duplicate email, password mismatch, missing required fields, terms not checked
- Thêm
beforeEachđể navigate tới page trước mỗi test - Viết helper function để generate unique test email cho mỗi test run