Testing Series (Part 4) - Beyond Functional: Visual, Accessibility, Performance & Contract Testing
🎯 Mục Tiêu Bài Viết
Ở 3 phần trước, chúng ta đã đi sâu vào Functional Testing — Unit, Integration, E2E, Playwright. Đó là nền tảng, nhưng chưa phải toàn bộ bức tranh.
Bài viết này mở rộng sang những mảng nhiều Frontend dev chưa chạm tới nhưng lại cực kỳ quan trọng nếu bạn muốn thực sự lên Senior.
Senior không chỉ biết viết E2E test. Senior hiểu toàn bộ testing landscape và biết khi nào dùng loại test nào.
Series Navigation
Part 1 → Testing Pyramid & E2E nằm ở đâu
Part 2 → E2E Architecture: Page Object, Debug, Flaky Test
Part 3 → Playwright Deep Dive: Internal & Parallel
Part 4 → (bài này) Beyond Functional: Visual, A11y, Perf, Contract
🏗️ 1. Big Picture — Toàn Bộ Software Testing
SOFTWARE TESTING
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
Functional Testing Non-Functional Testing Test Management
(Part 1-3 đã cover) (Part 4 — bài này)
│ │ │
▼ ▼ ▼
┌─────┴─────┐ ┌──────┴──────┐ ┌─────┴─────┐
│Unit │ │Performance │ │Strategy │
│Integration│ │Security │ │Planning │
│E2E │ │Accessibility│ │Reporting │
└───────────┘ │Visual │ └───────────┘
│Contract │
└─────────────┘
Bạn Đang Ở Đâu?
✅ Functional Testing → Đã cover ở Part 1-3
├─ Unit Test
├─ Integration Test
└─ E2E Test (Playwright)
🔥 Non-Functional Testing → Bài này
├─ Visual Regression Testing
├─ Accessibility Testing
├─ Performance Testing
├─ Security Testing
└─ Contract Testing
📋 Test Management → Senior mindset
├─ Test Strategy
└─ Test Automation Architecture
🎨 2. Visual Regression Testing
Vấn Đề Thực Tế
Dev A merge PR → CSS change nhỏ
│
▼
Button dịch sang phải 2px
Header font thay đổi
Modal layout vỡ trên mobile
│
▼
KHÔNG có test nào catch được
│
▼
QA phát hiện sau 3 ngày
User phàn nàn
Unit test không catch layout bug. E2E test cũng không.
Visual Testing Hoạt Động Thế Nào?
Step 1: Capture Baseline
+-------------------+
| Screenshot BEFORE |
| (approved state) |
+---------+---------+
│
Step 2: Capture Current
+---------+---------+
│
+-------------------+
| Screenshot AFTER |
| (after code change)|
+---------+---------+
│
Step 3: Pixel Diff
+---------+---------+
│
+-------------------+
| Compare pixels |
| Highlight diff |
+---------+---------+
│
+----+----+
│ │
▼ ▼
No diff Diff found
Pass ✅ Review 🔍
│
+----+----+
│ │
▼ ▼
Approve Reject
(update (fix bug)
baseline)
Tools
| Tool | Đặc điểm | Giá |
|---|---|---|
| Percy (BrowserStack) | CI integration tốt, cross-browser | Paid |
| Chromatic (Storybook) | Tích hợp Storybook, component-level | Free tier |
| Applitools | AI-powered diff, smart | Paid |
| Playwright (built-in) | toHaveScreenshot() |
Free |
| BackstopJS | Open source, config-based | Free |
Playwright Visual Testing (Free & Đơn Giản Nhất)
import { test, expect } from "@playwright/test";
test("homepage visual check", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
// So sánh toàn bộ page
await expect(page).toHaveScreenshot("homepage.png");
});
test("login form visual check", async ({ page }) => {
await page.goto("/login");
// So sánh 1 component cụ thể
const form = page.locator('[data-testid="login-form"]');
await expect(form).toHaveScreenshot("login-form.png");
});
test("dashboard responsive check", async ({ page }) => {
// Mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/dashboard");
await expect(page).toHaveScreenshot("dashboard-mobile.png");
// Desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(page).toHaveScreenshot("dashboard-desktop.png");
});
Khi Nào Dùng?
✅ Dùng Visual Test khi:
├─ Component library / Design system
├─ Landing page / Marketing page
├─ Responsive layout critical
├─ After CSS refactor
└─ Cross-browser rendering
❌ Không dùng cho:
├─ Dynamic content (thay đổi liên tục)
├─ Data-driven pages (số liệu thay đổi)
└─ Animation-heavy pages
♿ 3. Accessibility Testing (A11y)
Tại Sao Quan Trọng?
15% dân số thế giới có disability
│
▼
Nếu app không accessible:
├─ Blind users không dùng được (screen reader)
├─ Motor disability không navigate được (keyboard)
├─ Color blind không đọc được (contrast)
└─ Legal risk (ADA lawsuit — phổ biến ở US/EU)
A11y Testing Scope
Accessibility Testing
│
├── 1. Screen Reader
│ ├── ARIA labels đúng?
│ ├── Heading structure (h1 → h2 → h3)?
│ └── Alt text cho images?
│
├── 2. Keyboard Navigation
│ ├── Tab order logic?
│ ├── Focus visible?
│ └── Escape close modal?
│
├── 3. Color & Contrast
│ ├── Text contrast ratio ≥ 4.5:1?
│ ├── Không dùng color alone truyền thông tin?
│ └── Focus indicator visible?
│
└── 4. Semantic HTML
├── Button vs div (clickable)?
├── Form labels?
└── Landmark regions (nav, main, footer)?
Tools
| Tool | Loại | Cách dùng |
|---|---|---|
| axe-core | Library | Tích hợp vào test runner |
| Lighthouse | CLI/Browser | Audit score |
| Pa11y | CLI | CI integration |
| @axe-core/playwright | Playwright plugin | Trong E2E test |
| eslint-plugin-jsx-a11y | Linter | Catch lúc code |
Tích Hợp A11y Vào Playwright
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("homepage has no a11y violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"]) // WCAG 2.0 Level A + AA
.analyze();
expect(results.violations).toEqual([]);
});
test("login form is accessible", async ({ page }) => {
await page.goto("/login");
const results = await new AxeBuilder({ page })
.include('[data-testid="login-form"]') // Chỉ scan form
.analyze();
expect(results.violations).toEqual([]);
});
Angular A11y Checklist
<!-- ❌ Bad -->
<div (click)="submit()">Submit</div>
<img src="logo.png" />
<input type="text" />
<!-- ✅ Good -->
<button (click)="submit()">Submit</button>
<img src="logo.png" alt="Company Logo" />
<label for="email">Email</label>
<input id="email" type="text" aria-required="true" />
✅ A11y Quick Wins cho Angular:
├─ Dùng <button> thay <div> cho clickable elements
├─ Thêm alt text cho tất cả images
├─ Thêm label cho tất cả form inputs
├─ Đảm bảo tab navigation logic
├─ Focus trap trong modal/dialog
├─ aria-live cho dynamic content
└─ Contrast ratio ≥ 4.5:1
⚡ 4. Performance Testing
Frontend Performance vs Backend Performance
Frontend Performance Backend Performance
(bạn nên biết) (backend team lo)
│ │
├─ Bundle size ├─ Load testing
├─ First Contentful Paint ├─ Stress testing
├─ Largest Contentful Paint ├─ Spike testing
├─ Time to Interactive └─ Soak testing
├─ Cumulative Layout Shift
└─ Network waterfall
Frontend Performance Testing
Measure Core Web Vitals:
LCP (Largest Contentful Paint)
├─ Mục tiêu: < 2.5s
└─ Main content render xong
FID (First Input Delay)
├─ Mục tiêu: < 100ms
└─ User click → browser respond
CLS (Cumulative Layout Shift)
├─ Mục tiêu: < 0.1
└─ Layout không nhảy lung tung
Tools
Frontend:
├─ Lighthouse (Google) — audit score
├─ WebPageTest — detailed waterfall
├─ Chrome DevTools Performance tab
└─ Playwright + Performance API
Backend / API:
├─ k6 — load testing (JavaScript-based)
├─ JMeter — enterprise load testing
├─ Artillery — modern load testing
└─ Gatling — Scala-based
Lighthouse Trong CI (Tự Động)
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci && npm run build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:4200/
http://localhost:4200/login
budgetPath: ./lighthouse-budget.json
// lighthouse-budget.json
[
{
"path": "/*",
"timings": [
{ "metric": "first-contentful-paint", "budget": 2000 },
{ "metric": "interactive", "budget": 3500 },
{ "metric": "largest-contentful-paint", "budget": 2500 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 500 }
]
}
]
k6 — API Load Testing (Frontend Dev Nên Biết)
// load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 50 }, // ramp up to 50 users
{ duration: "1m", target: 50 }, // stay at 50
{ duration: "30s", target: 100 }, // ramp up to 100
{ duration: "1m", target: 100 }, // stay at 100
{ duration: "30s", target: 0 }, // ramp down
],
};
export default function () {
const res = http.get("https://api.example.com/products");
check(res, {
"status is 200": (r) => r.status === 200,
"response time < 500ms": (r) => r.timings.duration < 500,
});
sleep(1);
}
// Run: k6 run load-test.js
🔐 5. Security Testing (Cơ Bản Cho Frontend)
Frontend Security Scope
Frontend Security Testing
│
├── 1. XSS (Cross-Site Scripting)
│ ├── User input → rendered as HTML?
│ └── innerHTML / [innerHTML] usage?
│
├── 2. Token Storage
│ ├── JWT trong localStorage? (❌ risky)
│ └── HttpOnly cookie? (✅ safer)
│
├── 3. CSP (Content Security Policy)
│ ├── Inline scripts blocked?
│ └─ External scripts whitelisted?
│
├── 4. Dependency Vulnerabilities
│ ├── npm audit
│ └── Snyk / Dependabot
│
└── 5. Sensitive Data Exposure
├── API keys trong source code?
├── Debug info trong production?
└── Source maps exposed?
Quick Security Audit
# Check npm vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Check with Snyk (more thorough)
npx snyk test
Angular-Specific Security
// ❌ Bad — XSS vulnerable
@Component({
template: `<div [innerHTML]="userInput"></div>`
})
// ✅ Good — Angular auto-sanitizes
@Component({
template: `<div></div>`
})
// ✅ Good — explicit sanitization
import { DomSanitizer } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) {}
getSafeHtml(html: string) {
return this.sanitizer.bypassSecurityTrustHtml(html);
// Chỉ dùng khi CHẮC CHẮN html an toàn
}
🤝 6. Contract Testing
Vấn Đề Thực Tế
Frontend team:
"API trả về { user: { name: 'John' } }"
Backend team deploy thay đổi:
"API giờ trả về { user: { fullName: 'John' } }"
→ Frontend BREAK
→ Không có test nào catch
→ Bug phát hiện ở production
Contract Testing Giải Quyết Gì?
Frontend Backend
│ │
▼ │
+----------------+ │
| Consumer | │
| Contract | │
| "I expect: | │
| GET /users | │
| returns | │
| { name: str } | │
+-------+--------+ │
│ │
│ share contract │
│ │
▼ ▼
+----------------+ +----------------+
| Contract | | Provider |
| Broker | ─────► | Verification |
| (Pact) | | "Do I match |
+----------------+ | the contract?"|
+----------------+
│
+----+----+
│ │
▼ ▼
Match ✅ Mismatch ❌
Deploy Block deploy
Tools
| Tool | Ecosystem | Đặc điểm |
|---|---|---|
| Pact | Polyglot | Phổ biến nhất, consumer-driven |
| Spring Cloud Contract | Java/Spring | Backend-driven |
| Specmatic | OpenAPI | Schema-based |
Khi Nào Cần?
✅ Cần Contract Testing khi:
├─ Frontend và Backend team KHÁC NHAU
├─ API thay đổi thường xuyên
├─ Microservices architecture
└─ Breaking change khó detect
❌ Không cần khi:
├─ Fullstack team (1 người làm cả FE + BE)
├─ API cực kỳ stable
└─ Có integration test cover đủ
🧪 7. API Testing Automation
Khác Gì E2E Test?
E2E Test:
Browser → Frontend → API → DB
(chậm, flaky, test UI + API)
API Test:
Test Runner → API → DB
(nhanh, stable, test API trực tiếp)
Architecture
+-------------------+
| API Test Suite |
| (Supertest/k6) |
+---------+---------+
│
│ HTTP requests
▼
+-------------------+
| Backend API |
+---------+---------+
│
▼
+-------------------+
| Database |
+-------------------+
Ví Dụ Với Playwright API Testing
import { test, expect } from "@playwright/test";
test.describe("User API", () => {
test("GET /api/users returns user list", async ({ request }) => {
const response = await request.get("/api/users");
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toBeInstanceOf(Array);
expect(users[0]).toHaveProperty("name");
expect(users[0]).toHaveProperty("email");
});
test("POST /api/users creates user", async ({ request }) => {
const response = await request.post("/api/users", {
data: {
name: "Test User",
email: `test-${Date.now()}@example.com`,
},
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.name).toBe("Test User");
});
test("GET /api/users/999 returns 404", async ({ request }) => {
const response = await request.get("/api/users/999");
expect(response.status()).toBe(404);
});
});
🗺️ 8. Test Automation Architecture — Dự Án Lớn
+--------------------------------------------------+
| TEST AUTOMATION PLATFORM |
| |
| +----------+ +----------+ +----------+ |
| | UI | | API | | Perf | |
| | Testing | | Testing | | Testing | |
| |Playwright| |Supertest | | k6 | |
| +----+-----+ +----+-----+ +----+-----+ |
| │ │ │ |
| +----+-----+ +----+-----+ +----+-----+ |
| | Visual | | Contract | | Security | |
| | Testing | | Testing | | Testing | |
| | Percy | | Pact | | Snyk | |
| +----------+ +----------+ +----------+ |
| |
| +--------------------------------------------+ |
| | Accessibility Testing | |
| | axe-core + Lighthouse | |
| +--------------------------------------------+ |
| |
+-------------------------+------------------------+
│
▼
+----------+----------+
| CI / CD |
| GitHub Actions / |
| GitLab CI |
+----------+----------+
│
▼
Deploy if ALL pass
🛣️ 9. Frontend Senior Testing Roadmap
Bạn đang ở đây
│
▼
┌─────────────────────────────────────────────────┐
│ │
│ ✅ DONE (Part 1-3) │
│ ├─ Unit Test (Jasmine/Jest) │
│ ├─ Integration Test │
│ ├─ E2E Test (Playwright) │
│ ├─ Page Object Model │
│ ├─ CI/CD Integration │
│ └─ Debug & Fix Flaky Tests │
│ │
│ 🔥 LEARNING NOW (Part 4 — bài này) │
│ ├─ Visual Regression Testing │
│ ├─ Accessibility Testing │
│ ├─ Performance Testing │
│ ├─ Security Testing (frontend scope) │
│ ├─ Contract Testing │
│ └─ API Testing Automation │
│ │
│ 🎯 NEXT LEVEL (Senior → Staff) │
│ ├─ Test Strategy Design │
│ ├─ Chaos Testing │
│ ├─ Test Automation Architecture │
│ └─ Testing Culture & Mentoring │
│ │
└─────────────────────────────────────────────────┘
Priority Cho Frontend Senior
Nếu thời gian có hạn, học theo thứ tự này:
1️⃣ Accessibility Testing (axe + Playwright)
→ Impact lớn, setup nhanh, ít dev biết
2️⃣ Visual Regression Testing (Playwright screenshot)
→ Catch layout bugs, free với Playwright
3️⃣ API Testing Automation (Playwright request API)
→ Nhanh, stable hơn E2E cho API validation
4️⃣ Performance Testing (Lighthouse CI)
→ Tự động monitor Core Web Vitals
Chỉ cần 4 cái này → hơn ~80% frontend dev về testing.
🎯 Checklist Tự Đánh Giá
Visual Testing
- Hiểu pixel diff hoạt động thế nào?
- Dùng được
toHaveScreenshot()trong Playwright? - Biết khi nào dùng Visual Test vs E2E Test?
Accessibility
- Biết WCAG 2.0 Level A/AA là gì?
- Tích hợp được axe-core vào Playwright?
- Check được keyboard navigation?
- Audit được contrast ratio?
Performance
- Biết Core Web Vitals (LCP, FID, CLS)?
- Setup được Lighthouse CI?
- Hiểu bundle size ảnh hưởng performance thế nào?
Contract & API Testing
- Hiểu Contract Testing giải quyết vấn đề gì?
- Viết được API test với Playwright request?
- Biết sự khác biệt API Test vs E2E Test?
Security
- Biết XSS prevention trong Angular?
- Chạy được
npm audit? - Hiểu token storage best practices?
📚 Tài Liệu Tham Khảo
- Visual: Playwright Visual Comparisons
- A11y: axe-core Playwright
- A11y: WCAG Quick Reference
- Perf: web.dev Core Web Vitals
- Perf: Lighthouse CI
- Contract: Pact Documentation
- k6: k6 Documentation
- Security: Angular Security Guide
💡 Câu Chốt Lõi
Functional Testing → "App hoạt động đúng không?"
Non-Functional Testing → "App hoạt động TỐT không?"
Visual → UI có vỡ layout không?
A11y → Mọi người đều dùng được không?
Performance → App có nhanh không?
Security → App có an toàn không?
Contract → FE và BE có đồng bộ không?
Senior biết testing KHÔNG CHỈ là Unit + E2E.
Senior hiểu toàn bộ bức tranh.
Đó là sự khác biệt thực sự.
“Quality is not an act, it is a habit.” — Aristotle