·

Tiếng Việt: E2E test script generation

E2E test script generation

Viết test E2E (end-to-end) bằng tay luôn là công việc tốn thời gian nhất trong vòng đời automation test: phải hiểu flow nghiệp vụ, tìm selector ổn định, xử lý timing, viết assertion đúng chỗ, rồi maintain khi UI đổi. Với sự hỗ trợ của AI/LLM (large language model), một QA Engineer có kinh nghiệm có thể biến việc này thành quy trình "mô tả flow → review script → điều chỉnh" nhanh hơn gấp nhiều lần so với gõ code từ đầu. Nhưng tốc độ chỉ có giá trị khi chất lượng script được kiểm soát chặt — AI sinh code rất "tự tin" nhưng không phải lúc nào cũng đúng ngữ cảnh dự án của bạn. Chủ đề này đi sâu vào cách mô tả flow, cách prompt cho từng framework (Playwright, Maestro, Robot Framework), và cách review script AI sinh ra một cách có hệ thống.

Làm sao mô tả User Flow cho AI để sinh Script E2E chính xác?

Chất lượng test script AI sinh ra phụ thuộc trực tiếp vào chất lượng mô tả flow bạn đưa vào prompt. Nếu bạn chỉ viết "viết test cho chức năng checkout", AI sẽ tự đoán các bước dựa trên kiến thức chung về e-commerce — và gần như chắc chắn sẽ sai lệch so với flow thực tế của sản phẩm bạn. Ngược lại, nếu bạn mô tả flow đủ chi tiết theo một cấu trúc chuẩn, AI có thể sinh ra script gần như runnable ngay từ lần đầu.

Mẫu (Template) Mô Tả User Flow

Một template mô tả flow hiệu quả nên có các phần: (1) Actor — ai là người thực hiện flow (guest, user đã login, admin...); (2) Precondition — trạng thái hệ thống trước khi flow bắt đầu; (3) Steps — các bước tuần tự, mỗi bước gồm hành động + phần tử UI liên quan; (4) Expected Result — kết quả mong đợi sau mỗi bước quan trọng hoặc ở cuối flow; (5) Data — dữ liệu cụ thể dùng trong flow (không để AI tự bịa); (6) Edge cases cần cover (nếu có).

Template dạng prompt bạn có thể tái sử dụng:

Actor: [vai trò người dùng]
Precondition: [trạng thái hệ thống/dữ liệu trước khi test]
URL bắt đầu: [đường dẫn]
Steps:
1. [hành động] trên [phần tử, có thể kèm data-testid nếu biết]
2. ...
Expected Result:
- Sau step X: [điều gì phải đúng trên UI/network/DB]
Data:
- [tên biến]: [giá trị hoặc cách sinh]
Edge cases cần thêm test riêng:
- [case 1]
- [case 2]

Ví Dụ: Mô Tả Flow Checkout

Ví dụ thực tế cho một flow checkout của website bán hàng:

Actor: Khách đã đăng nhập, có 1 sản phẩm trong cart
Precondition: User "qa_tester@shop.vn" đã tồn tại, cart chứa SKU "SHOE-001" x1, giá 590.000đ
URL bắt đầu: /cart

Steps:
1. Click nút "Tiến hành thanh toán" (data-testid="checkout-btn")
2. Ở trang /checkout, điền địa chỉ giao hàng:
   - Họ tên: Nguyễn Văn A
   - SĐT: 0901234567
   - Địa chỉ: 12 Nguyễn Huệ, Q1, TP.HCM
3. Chọn phương thức thanh toán "COD"
4. Click "Đặt hàng" (data-testid="place-order-btn")

Expected Result:
- Sau step 4: redirect sang /order-success/{orderId}
- Hiển thị mã đơn hàng dạng "ORD-xxxxxx"
- Gọi API POST /api/orders trả về status 201
- Cart badge trên header trở về 0

Data:
- SKU: SHOE-001, đơn giá: 590000, số lượng: 1

Edge cases cần thêm test riêng:
- Địa chỉ giao hàng để trống SĐT → hiển thị lỗi validation, không gọi API orders
- Chọn COD nhưng địa chỉ ngoài khu vực hỗ trợ → hiển thị thông báo không hỗ trợ giao hàng

Với mô tả này, bạn có thể prompt trực tiếp: "Dựa vào flow mô tả trên, viết một Playwright test bằng TypeScript, dùng Page Object Model, đặt file tại tests/checkout/checkout-cod.spec.ts". Kết quả AI trả về sẽ sát với thực tế hơn rất nhiều so với prompt mơ hồ.

Những Gì Cần Có Trong Mô Tả Flow

Ngoài 6 phần trong template, khi mô tả flow cho AI bạn nên chú ý thêm: nêu rõ selector chiến lược nếu dự án đã có convention (ví dụ luôn dùng data-testid, không dùng CSS class); chỉ rõ những lời gọi API cần được assert (không chỉ assert UI); nêu rõ timing đặc biệt (có loading spinner, có debounce, có polling); và nếu flow có phần random/non-deterministic (giá thay đổi theo khuyến mãi, ngày giờ hiện tại...) thì phải nói rõ cách AI nên xử lý (mock hay để linh động).

Mẹo: Đừng viết mô tả flow bằng văn xuôi tự do rồi hy vọng AI hiểu đúng ý — hãy luôn dùng cấu trúc Steps/Expected Result dạng danh sách có số thứ tự. Kinh nghiệm thực tế cho thấy khi Expected Result được viết tách riêng theo từng step (thay vì gộp chung ở cuối), AI sinh assertion đúng vị trí hơn hẳn, vì nó biết chính xác assertion nào gắn với hành động nào.

Làm sao sinh Playwright Test đáng tin cậy — Selector, Assertion, Fixture — bằng AI?

Playwright là framework E2E phổ biến nhất hiện nay cho web, và cũng là framework mà AI được train nhiều dữ liệu nhất nên chất lượng code sinh ra thường khá tốt về mặt syntax. Vấn đề nằm ở chỗ khác: AI có xu hướng chọn selector giòn (brittle) như CSS class hoặc nth-child, viết assertion hời hợt (chỉ check toBeVisible()), và bỏ qua fixture/setup-teardown nếu bạn không yêu cầu rõ.

Hướng Dẫn AI Dùng Selector Bền Vững (Resilient)

Playwright Locator API hỗ trợ nhiều chiến lược selector, nhưng không phải chiến lược nào cũng bền theo thời gian. Thứ tự ưu tiên nên áp cho AI: getByTestId() > getByRole() với accessible name > getByLabel()/getByPlaceholder() cho form > tuyệt đối tránh CSS selector dựa trên class hoặc cấu trúc DOM (.container > div:nth-child(3)).

Bạn nên chèn rule này trực tiếp vào prompt hoặc vào file context (CLAUDE.md/system prompt của tool):

Quy tắc chọn selector khi viết Playwright test:
1. Ưu tiên page.getByTestId('...') nếu element có data-testid
2. Nếu không có testid, dùng page.getByRole('button', { name: '...' })
3. Với input, dùng page.getByLabel('...') hoặc getByPlaceholder('...')
4. TUYỆT ĐỐI không dùng CSS class selector (.btn-primary) hoặc XPath
5. Không dùng waitForTimeout() cố định — dùng expect().toBeVisible() 
   hoặc waitForResponse() để chờ điều kiện thật

Ví Dụ Prompt Sinh Test Login Kèm Fixture

Một prompt đầy đủ để sinh test login có fixture:

Viết Playwright test (TypeScript) cho flow login, yêu cầu:
- Dùng custom fixture `authPage` đã export từ tests/fixtures/auth.fixture.ts,
  fixture này expose sẵn instance LoginPage (Page Object)
- Test case:
  1. "login thành công với tài khoản hợp lệ" — dùng user 
     process.env.TEST_USER_EMAIL / TEST_USER_PASSWORD
  2. "login thất bại hiển thị lỗi khi sai password"
- Selector: chỉ dùng getByTestId, không dùng CSS class
- Assertion: check redirect URL, check localStorage có key "authToken",
  và check API POST /api/auth/login trả 200 (dùng page.waitForResponse)
- Đặt file tại tests/auth/login.spec.ts

Code Playwright thực tế phù hợp với prompt trên:

// tests/auth/login.spec.ts
import { test, expect } from '../fixtures/auth.fixture';

test.describe('Login flow', () => {
  test('login thành công với tài khoản hợp lệ', async ({ authPage, page }) => {
    const responsePromise = page.waitForResponse(
      (res) => res.url().includes('/api/auth/login') && res.request().method() === 'POST'
    );

    await authPage.login(
      process.env.TEST_USER_EMAIL!,
      process.env.TEST_USER_PASSWORD!
    );

    const response = await responsePromise;
    expect(response.status()).toBe(200);

    await expect(page).toHaveURL('/dashboard');
    const token = await page.evaluate(() => localStorage.getItem('authToken'));
    expect(token).not.toBeNull();
  });

  test('login thất bại hiển thị lỗi khi sai password', async ({ authPage, page }) => {
    await authPage.login(process.env.TEST_USER_EMAIL!, 'wrong-password');

    await expect(page.getByTestId('login-error')).toBeVisible();
    await expect(page.getByTestId('login-error')).toHaveText(/sai email hoặc mật khẩu/i);
    await expect(page).toHaveURL('/login');
  });
});

Prompt Cho Fixture Và Setup/Teardown

Nếu project chưa có fixture, bạn có thể yêu cầu AI sinh luôn fixture cùng lúc:

Sinh thêm file tests/fixtures/auth.fixture.ts, extend từ @playwright/test:
- fixture "authPage" trả về instance của class LoginPage (import từ ../pages/LoginPage)
- Trước mỗi test: goto('/login')
- Sau mỗi test: nếu localStorage có authToken, gọi API DELETE /api/auth/session để logout sạch

Việc yêu cầu rõ "sau mỗi test làm gì" (teardown) là bước rất nhiều người quên prompt, dẫn đến AI chỉ sinh phần setup mà bỏ qua dọn dữ liệu — về lâu dài gây ra test pollution giữa các lần chạy.

Assert Network Call, Không Chỉ UI State

Một lỗi phổ biến của test AI sinh là chỉ assert những gì hiển thị trên UI (ví dụ toBeVisible()), bỏ qua việc xác nhận backend thực sự được gọi đúng. Điều này khiến test có thể pass giả (false positive) khi UI hiển thị đúng nhờ optimistic update nhưng API thực tế lỗi. Hãy luôn yêu cầu AI kết hợp page.waitForResponse() hoặc route interception (page.route()) để verify request/response thực tế:

test('xóa item trong cart gọi đúng API', async ({ page }) => {
  const deleteRequest = page.waitForRequest(
    (req) => req.url().includes('/api/cart/items/') && req.method() === 'DELETE'
  );

  await page.getByTestId('remove-item-btn').click();

  const request = await deleteRequest;
  expect(request.url()).toContain('/api/cart/items/SHOE-001');
});

Mẹo: Khi prompt AI viết Playwright test, luôn thêm câu "assert cả network request lẫn UI state, không chỉ UI" ngay trong prompt gốc — nếu không, AI có xu hướng chỉ generate assertion nhìn thấy được (visible text, class active...) vì đó là phần "dễ suy luận" nhất từ mô tả flow của bạn.

Làm sao sinh Maestro Mobile E2E Flow với sự hỗ trợ của AI?

Maestro là công cụ E2E cho mobile (iOS/Android) dùng YAML để định nghĩa flow, khá khác biệt so với Playwright vì không viết bằng code lập trình truyền thống mà bằng cấu trúc khai báo (declarative). AI cần được cung cấp đúng "vocabulary" của Maestro để không sinh ra cú pháp sai hoặc lẫn với cú pháp của Appium/Detox.

Cung Cấp Context Maestro Cho AI

Vì Maestro là công cụ tương đối mới và ít phổ biến hơn Playwright, model có thể không nhớ chính xác toàn bộ command set. Cách an toàn nhất là dán kèm một đoạn tóm tắt command Maestro thường dùng vào prompt, hoặc trỏ AI đọc file flow mẫu đã có trong repo:

Đây là các command Maestro tôi dùng trong project (tham khảo để giữ đúng cú pháp):
- launchApp
- tapOn: { id: "..." } hoặc tapOn: "Text hiển thị"
- inputText: "..."
- assertVisible: { id: "..." }
- assertNotVisible
- swipe: { direction: UP/DOWN }
- waitForAnimationToEnd
- takeScreenshot: name

Viết một Maestro flow YAML cho: mở app, đăng nhập bằng email/password, 
vào màn Home, kiểm tra danh sách sản phẩm hiển thị ít nhất 1 item.
appId: vn.shop.mobile

Kết quả mong đợi:

appId: vn.shop.mobile
---
- launchApp
- tapOn:
    id: "email_input"
- inputText: "qa_tester@shop.vn"
- tapOn:
    id: "password_input"
- inputText: "Test@12345"
- tapOn:
    id: "login_button"
- waitForAnimationToEnd
- assertVisible:
    id: "home_screen"
- assertVisible:
    id: "product_list"
- assertVisible:
    text: ".*"
    index: 0

Sinh Maestro Flow Có Tham Số (Parameterized)

Maestro hỗ trợ biến qua ${OUTPUT.var} hoặc truyền env bằng -e. Khi cần chạy cùng flow cho nhiều tài khoản/data set, hãy yêu cầu AI parameterize ngay từ đầu:

Sửa flow login trên để dùng biến môi trường thay vì hard-code:
- Email lấy từ ${TEST_EMAIL}
- Password lấy từ ${TEST_PASSWORD}
Giữ nguyên cấu trúc còn lại.
appId: vn.shop.mobile
---
- launchApp
- tapOn:
    id: "email_input"
- inputText: "${TEST_EMAIL}"
- tapOn:
    id: "password_input"
- inputText: "${TEST_PASSWORD}"
- tapOn:
    id: "login_button"

Chạy bằng: maestro test login.yaml -e TEST_EMAIL=qa_tester@shop.vn -e TEST_PASSWORD=Test@12345.

Xử Lý Assertion Trong Maestro

Maestro assertion đơn giản hơn Playwright nhiều (không có matcher phong phú như toHaveText, toContainText), vì vậy khi mô tả expected result cho AI, bạn cần "dịch" các assertion phức tạp thành các bước assertVisible/assertNotVisible tương ứng, có thể kèm retryTapIfNoChange hoặc extendedWaitUntil khi UI có animation/loading:

- extendedWaitUntil:
    visible:
      id: "order_success_banner"
    timeout: 10000

Mẹo: Vì Maestro test chạy trên thiết bị/emulator thật với animation, hãy luôn nhắc AI chèn waitForAnimationToEnd sau các hành động điều hướng (chuyển màn hình, mở modal) — thiếu bước này là nguyên nhân số một gây flaky trên Maestro flow AI sinh ra.

Làm sao sinh Robot Framework Keyword-Driven Test bằng AI?

Robot Framework theo mô hình keyword-driven, khác hẳn Playwright/Maestro vì code test được viết dưới dạng bảng keyword đọc gần giống ngôn ngữ tự nhiên. Rủi ro lớn nhất khi dùng AI sinh Robot Framework test là AI "bịa" ra keyword không tồn tại trong project — vì cú pháp Robot Framework rất tự nhiên nên AI dễ đoán mò tên keyword nghe hợp lý nhưng sai thực tế.

Cung Cấp Keyword Library Cho AI

Trước khi prompt sinh test case, hãy luôn cung cấp danh sách keyword đã có (từ file .resource hoặc thư viện Python custom):

Đây là các custom keyword đã định nghĩa trong resources/checkout_keywords.robot:
- Login With Valid Credentials    [Arguments]    ${email}    ${password}
- Add Product To Cart             [Arguments]    ${sku}    ${quantity}
- Go To Checkout Page
- Fill Shipping Address           [Arguments]    ${name}    ${phone}    ${address}
- Select Payment Method           [Arguments]    ${method}
- Place Order
- Order Should Be Successful
- Order Should Fail With Message  [Arguments]    ${expected_message}

Chỉ sử dụng các keyword trên, KHÔNG tạo keyword mới trừ khi tôi yêu cầu.

Ví Dụ Prompt Và Output Sinh Robot Framework

Viết test case Robot Framework cho flow checkout COD thành công, dùng
đúng các keyword trong resources/checkout_keywords.robot ở trên.
Đặt trong file tests/checkout/checkout_cod.robot, dùng Test Tags "checkout" "smoke".
*** Settings ***
Resource    ../../resources/checkout_keywords.robot
Suite Setup    Login With Valid Credentials    qa_tester@shop.vn    Test@12345

*** Test Cases ***
Checkout Thanh Cong Voi Phuong Thuc COD
    [Tags]    checkout    smoke
    Add Product To Cart    SHOE-001    1
    Go To Checkout Page
    Fill Shipping Address    Nguyen Van A    0901234567    12 Nguyen Hue, Q1, TP.HCM
    Select Payment Method    COD
    Place Order
    Order Should Be Successful

Ngăn AI "Bịa" Keyword

Ngoài việc liệt kê keyword có sẵn, bạn nên thêm một constraint rõ ràng ở cuối prompt, và đây là bước hầu như bắt buộc với Robot Framework:

Nếu để hoàn thành test case này cần một keyword chưa tồn tại trong danh sách,
KHÔNG tự tạo keyword đó trong file test. Hãy dừng lại và liệt kê rõ 
keyword nào còn thiếu, mô tả Arguments và hành vi mong muốn của keyword đó,
để tôi tạo trong resource file trước.

Đây là kỹ thuật "circuit breaker" cho AI — buộc nó báo cáo thiếu sót thay vì tự bịa ra keyword ảo, tránh trường hợp bạn chạy test và nhận lỗi No keyword with name '...' found chỉ vì AI đoán sai tên.

Mẹo: Luôn giữ một file keywords_catalog.md hoặc trích xuất tự động danh sách keyword bằng libdoc rồi dán nguyên văn vào prompt — đừng dựa vào việc AI "nhớ" project của bạn từ những lần chat trước, vì context có thể bị cắt hoặc quên giữa các session.

Làm sao Review Script E2E do AI sinh để đảm bảo đúng và ít rủi ro Flaky?

Sinh script nhanh chỉ là nửa đầu công việc — nửa sau, quan trọng không kém, là review để đảm bảo script không chỉ "chạy được" mà còn đúng về logic nghiệp vụ và không tạo ra flaky test (test không ổn định, lúc pass lúc fail dù code không đổi) khi chạy nhiều lần trên CI.

Checklist Review Test AI Sinh

Một checklist review nên bao gồm tối thiểu các điểm sau:

  1. Selector có bền vững không (test ID, role — không phải CSS class/nth-child)?
  2. waitForTimeout()/sleep cố định nào không — đây luôn là dấu hiệu flaky tiềm ẩn?
  3. Assertion có đủ sâu không (network + UI + data), hay chỉ check hời hợt toBeVisible()?
  4. Test có độc lập (isolated) không — có phụ thuộc thứ tự chạy hoặc dữ liệu từ test khác?
  5. Setup/teardown có dọn dữ liệu sạch sau khi chạy không?
  6. Có hard-code data nhạy cảm (email thật, số thẻ, token) không?
  7. Tên test case và tên file có theo đúng convention của project không?
  8. AI có tự bịa selector/API endpoint/keyword nào không có trong codebase thực tế không?

Dùng AI Để Review Test Do AI Sinh

Một kỹ thuật hiệu quả là dùng chính AI (có thể là một session/model khác, hoặc cùng model nhưng với prompt độc lập) để đóng vai reviewer, tách biệt hoàn toàn với vai trò sinh code ban đầu:

Bạn là senior QA engineer đang review Playwright test dưới đây.
Hãy đánh giá theo các tiêu chí:
1. Selector: có brittle không?
2. Assertion: có đủ để bắt lỗi thực (network + UI), hay chỉ hời hợt?
3. Flakiness risk: có waitForTimeout, race condition, hoặc thiếu wait điều kiện?
4. Isolation: test này có phụ thuộc state từ test khác không?
5. Data: có hard-code data nhạy cảm không?

Với mỗi vấn đề tìm thấy, chỉ rõ dòng code, giải thích rủi ro, và đề xuất fix cụ thể.

[dán code test vào đây]

Việc tách vai trò "sinh" và "review" ra hai lượt prompt riêng (dù cùng AI) giúp giảm đáng kể bias xác nhận (confirmation bias) — vì khi AI vừa viết code, nó có xu hướng tự bảo vệ output của mình nếu được hỏi "code này có ổn không" trong cùng một luồng hội thoại.

Đánh Giá (Scoring) Test AI Sinh

Để review có tính hệ thống hơn là cảm tính, bạn có thể yêu cầu AI (hoặc tự làm) chấm điểm test theo thang điểm cho từng tiêu chí, ví dụ 0-2 điểm mỗi mục (Selector resilience, Assertion depth, Isolation, Flakiness risk, Convention compliance) — tổng điểm dưới một ngưỡng (ví dụ 7/10) thì bắt buộc phải sửa trước khi merge. Cách làm này đặc biệt hữu ích khi team có nhiều người dùng AI sinh test với chất lượng không đồng đều, giúp chuẩn hóa "bar" chấp nhận chung.

Mẹo: Đừng review test AI sinh bằng cách chỉ đọc lướt code — hãy chạy thử với --repeat-each=5 (Playwright) hoặc tương đương trên CI trước khi merge. Một test "trông sạch" về code style vẫn có thể flaky 1/5 lần vì race condition ẩn mà mắt thường không phát hiện được khi đọc tĩnh (static review).