Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 46 — Test Automation Framework Architecture

Mở đầu — vì sao bài này quan trọng

Rất nhiều đội QA ở Việt Nam bắt đầu automation bằng cách... viết đại. Một bạn tester học Selenium xong, mở IDE lên, và viết một file dài 300 dòng: mở trình duyệt, driver.findElement(By.id("username")).sendKeys(...), click, Thread.sleep(3000), assert. Ba tháng sau, họ có 200 test case như thế. Rồi giao diện đổi nút "Đăng nhập" từ id="btn-login" sang id="signin", và họ phải sửa tay 47 chỗ. Sáu tháng sau, không ai dám chạm vào bộ test đó nữa, nó "flaky" (lúc pass lúc fail không rõ lý do), và cả team quay lại test tay. Đây là cái chết kinh điển của một dự án automation — không phải vì công cụ tệ, mà vì không có kiến trúc.

Kiến trúc framework automation (Test Automation Framework Architecture) chính là thứ quyết định bộ test của bạn sống được 5 năm hay chết sau 5 tháng. Với vai trò QA Lead, đây là một trong những quyết định kỹ thuật quan trọng nhất bạn đưa ra: nó ảnh hưởng đến chi phí bảo trì, tốc độ onboard người mới, khả năng scale, và cả việc dev có tin tưởng kết quả test hay không. Một framework tốt biến automation từ "gánh nặng nợ kỹ thuật" thành "tài sản sinh lời". Bài này không dạy bạn viết test — nó dạy bạn thiết kế cái khung để hàng trăm test sống chung một cách bền vững.

Khái niệm cốt lõi

Ý tưởng trung tâm của kiến trúc framework là phân tầng (layering) — tách biệt "cái gì cần kiểm thử" khỏi "kiểm thử bằng cách nào" khỏi "tương tác với trình duyệt ra sao". Mỗi tầng chỉ biết tầng ngay dưới nó, và mỗi tầng có một lý do thay đổi riêng. Nguyên tắc vàng: khi UI đổi, bạn chỉ sửa một tầng; khi nghiệp vụ đổi, bạn chỉ sửa một tầng khác.

Bốn tầng kinh điển

Hãy hình dung framework như một tòa nhà bốn tầng, dữ liệu và lệnh đi từ trên xuống:

1. Test cases        (Gherkin / spec files)      — MÔ TẢ nghiệp vụ
  • Step definitions / Page Objects — DIỄN DỊCH nghiệp vụ thành thao tác
  • Action helpers — GÓI thao tác lặp lại thành hàm dùng chung
  • Driver / WebDriver / Appium — GIAO TIẾP với trình duyệt/thiết bị

Tầng 1 — Test cases (kịch bản). Đây là tầng gần business nhất, viết bằng ngôn ngữ ai đọc cũng hiểu. Với BDD, nó là các file .feature viết bằng Gherkin: Given người dùng ở trang đăng nhập / When nhập đúng tài khoản / Then vào được trang chủ. Không có code trình duyệt ở đây. BA, PO, thậm chí khách hàng đọc được. Tầng này trả lời câu hỏi "chúng ta kiểm thử điều gì?".

Tầng 2 — Step definitions & Page Objects. Đây là bộ dịch. Mỗi câu Gherkin ("người dùng nhập đúng tài khoản") được map vào một hàm, và hàm đó gọi tới Page Object — một class đại diện cho một trang/màn hình. Page Object đóng gói locator (cách tìm phần tử) và hành vi của trang đó. Ví dụ LoginPage.login(user, pass). Điểm mấu chốt: mọi thông tin về locator của trang Login chỉ nằm ở một chỗ duy nhất — trong LoginPage. UI đổi id nút login? Bạn sửa đúng một dòng trong LoginPage, cả trăm test đang gọi LoginPage.login() tự động chạy đúng trở lại.

Tầng 3 — Action helpers (tiện ích thao tác). Đây là tầng gói các thao tác lặp đi lặp lại và các thao tác "khó" thành hàm ổn định: waitAndClick(element) tự đợi phần tử hiển thị rồi mới click, safeType(), scrollIntoView(), waitForApiIdle(), upload file, xử lý popup... Nếu không có tầng này, mỗi Page Object sẽ tự viết logic chờ đợi, và bạn sẽ có 50 cách chờ khác nhau — nguồn gốc của flakiness. Tầng này biến "sự bất định của trình duyệt" thành các hàm đáng tin cậy.

Tầng 4 — Driver. Tầng thấp nhất, trực tiếp nói chuyện với WebDriver (web), Appium (mobile), hoặc engine của Playwright/Cypress. Tầng này quản lý vòng đời driver: khởi tạo, cấu hình (headless hay không, độ phân giải), và dọn dẹp (driver.quit()). Lý tưởng nhất là tầng 3 trở lên không bao giờ gọi trực tiếp driver thô — chúng đi qua các helper. Nhờ vậy, đổi từ Selenium sang Playwright chỉ ảnh hưởng tầng 3–4.

Các thành phần đi kèm (không phải "tầng" nhưng thiết yếu)

Ngoài bốn tầng, một framework trưởng thành còn có các module ngang (cross-cutting):

  • Config: quản lý môi trường (dev/staging/prod), URL, credential — không hardcode.
  • Test Data: dữ liệu test tách khỏi code, đọc từ JSON/Excel/DB/API. Test không nên "chôn" dữ liệu bên trong.
  • Reporting: Allure, ExtentReports — biến kết quả thành báo cáo có screenshot khi fail.
  • Logging & Screenshot-on-failure: khi một test đỏ, phải biết ngay tại sao.
  • CI hook: khả năng chạy trong pipeline (chi tiết sâu ở Bài 17).

Ba lựa chọn kiểu framework

  • Data-Driven: một luồng test, chạy với nhiều bộ dữ liệu (100 tài khoản khác nhau).
  • Keyword-Driven: hành động được mô tả bằng "từ khóa" trong bảng, phù hợp khi tester ít code.
  • BDD (Behavior-Driven): dùng Gherkin, tối ưu cho giao tiếp với business.
Trong thực tế 2024–2026, xu hướng phổ biến nhất ở các công ty product Việt Nam là BDD + Page Object Model + Data-Driven kết hợp, chạy trên Playwright hoặc Selenium.

Tình huống thực tế

Tình huống 1 — Sàn TMĐT tại TP.HCM và cái giá của "không phân tầng"

Một công ty thương mại điện tử tầm trung ở Sài Gòn (khoảng 40 kỹ sư, gọi là ShopViet) có bộ 600 test Selenium viết theo kiểu "linear script" — locator rải khắp nơi, không có Page Object. Khi họ redesign trang giỏ hàng và trang thanh toán, 214 test fail cùng lúc. Đội QA 4 người mất 3 tuần chỉ để sửa locator, trong khi release bị treo. Tính ra chi phí gián tiếp (delay release mùa sale 9/9) lên tới hàng trăm triệu đồng.

Sau sự cố, QA Lead của họ tái cấu trúc theo Page Object Model: gom toàn bộ locator vào các class CartPage, CheckoutPage. Sáu tháng sau, một đợt redesign tương tự xảy ra — lần này chỉ mất 1,5 ngày để cập nhật, vì locator chỉ nằm ở hai file. Bài học: chi phí không nằm ở lúc viết test lần đầu, mà ở lúc bảo trì. Phân tầng chính là bảo hiểm cho tương lai.

Tình huống 2 — Fintech và tầng Action Helper cứu bộ test khỏi flaky

Một ví điện tử (giả định tên PayNhanh, ~200 nhân sự) có bộ test luôn "chập chờn": tỷ lệ fail giả (false fail) khoảng 18% mỗi lần chạy CI, vì trang tải chậm và mỗi Page Object tự viết Thread.sleep() với thời gian khác nhau. Dev mất niềm tin, bắt đầu bỏ qua kết quả QA — dấu hiệu chết của automation.

QA Lead cho dừng viết test mới trong 2 tuần, tập trung xây tầng Action Helper: thay toàn bộ sleep bằng explicit wait thông minh (waitForElementClickable, waitForNetworkIdle), gói vào một class UIActions duy nhất mà mọi Page Object phải gọi qua. Sau đó, tỷ lệ false fail rớt từ 18% xuống dưới 2%. Dev tin tưởng trở lại, quy tắc "test đỏ thì chặn merge" được khôi phục. Bài học: flakiness thường không phải lỗi công cụ, mà là hậu quả của việc thiếu một tầng thao tác chuẩn hóa. Một helper tốt còn giá trị hơn 100 test viết vội.

Tình huống 3 — Startup chọn kiến trúc cho phép đổi công cụ

Một startup logistics ở Hà Nội xây framework từ đầu năm 2024. QA Lead ngay từ ngày đầu áp dụng nguyên tắc "Page Object không gọi thẳng driver". Đến giữa 2025, họ quyết định chuyển từ Selenium sang Playwright để test nhanh và ổn định hơn. Vì tầng driver được cô lập sau các Action Helper, họ chỉ phải viết lại tầng 3–4 (helper + driver init), còn toàn bộ Page Object và file .feature (khoảng 85% khối lượng code) giữ nguyên. Việc chuyển đổi mất 3 tuần thay vì làm lại từ đầu. Bài học: kiến trúc tốt bảo vệ bạn khỏi rủi ro công nghệ — công cụ có thể lỗi thời, nhưng nghiệp vụ và cấu trúc thì bền.

Hướng dẫn từng bước

Đây là lộ trình dựng một framework có kiến trúc đúng từ số 0:

Bước 1 — Chốt tech stack và kiểu framework. Chọn ngôn ngữ theo team (Java/TestNG, JavaScript/Playwright, Python/pytest). Chọn kiểu: nếu cần business đọc được thì BDD (Cucumber/Behave); nếu team thuần kỹ sư thì Page Object thuần cũng đủ. Đừng chọn cái "hot" — chọn cái team bảo trì được.

Bước 2 — Dựng khung thư mục phản ánh các tầng. Ví dụ:

/features        → tầng 1 (.feature)
/steps           → tầng 2 (step definitions)
/pages           → tầng 2 (page objects)
/utils           → tầng 3 (action helpers)
/core            → tầng 4 (driver factory)
/config          → cấu hình môi trường
/testdata        → dữ liệu test
/reports         → báo cáo output
Cấu trúc thư mục chính là tài liệu kiến trúc trực quan nhất cho người mới.

Bước 3 — Xây tầng 4 (Driver Factory) trước tiên. Viết một class khởi tạo driver theo config (trình duyệt gì, headless không, độ phân giải), dùng pattern Factory. Đảm bảo mỗi test lấy driver qua factory, không tự new.

Bước 4 — Xây tầng 3 (Action Helpers). Ưu tiên các hàm chờ đợi thông minh: waitAndClick, waitAndType, getText, isVisible. Cấm tuyệt đối sleep cứng. Đây là nền móng chống flaky — làm kỹ ngay từ đầu.

Bước 5 — Xây một Page Object mẫu (tầng 2). Chọn một trang đơn giản (Login), tạo LoginPage chứa locator ở đầu class và các method hành vi. Method chỉ gọi Action Helper, không gọi driver thô.

Bước 6 — Viết một kịch bản đầu-cuối (tầng 1). Một file .feature + step definition map xuống LoginPage. Chạy xanh. Giờ bạn đã có một "lát cắt dọc" xuyên cả bốn tầng — bằng chứng kiến trúc hoạt động.

Bước 7 — Tách config và test data ra ngoài. Chuyển URL, credential, dữ liệu vào file config/JSON. Không hardcode.

Bước 8 — Gắn Reporting + screenshot-on-failure. Tích hợp Allure/ExtentReports. Đặt hook chụp màn hình mỗi khi test fail.

Bước 9 — Đưa vào CI. Cho chạy trên pipeline (chi tiết ở Bài 17). Từ đây framework mới thực sự tạo giá trị: chạy tự động mỗi lần merge.

Bước 10 — Viết tài liệu chuẩn đóng góp (contribution guide). Quy định rõ: locator để đâu, khi nào thêm helper, cách đặt tên. Không có quy ước, framework sẽ "phân rã" khi nhiều người cùng viết.

Lỗi thường gặp & mẹo

Lỗi 1 — Locator rải khắp nơi. Đây là lỗi số một. Nếu bạn thấy By.id(...) xuất hiện trong file test hay step definition, kiến trúc đã sai. Locator chỉ được sống trong Page Object. Mẹo: code review chặn ngay mọi PR có locator ngoài /pages.

Lỗi 2 — Lạm dụng Thread.sleep(). Sleep cứng vừa làm test chậm, vừa gây flaky (chờ 3s nhưng máy CI tải mất 4s → fail). Mẹo: cấm sleep trong quy ước team, chỉ dùng explicit wait qua Action Helper.

Lỗi 3 — Page Object "béo phì" ôm luôn logic assert. Page Object chỉ nên trả về trạng thái/dữ liệu; việc assert (kiểm tra đúng/sai) thuộc về tầng test. Trộn hai thứ khiến Page Object khó tái sử dụng.

Lỗi 4 — Test phụ thuộc lẫn nhau. Test B chỉ chạy được sau khi test A tạo dữ liệu. Rất nguy hiểm khi chạy song song. Mẹo: mỗi test tự chuẩn bị và dọn dẹp dữ liệu của mình (độc lập, atomic). Tận dụng API để setup dữ liệu nhanh thay vì click qua UI.

Lỗi 5 — Không có chiến lược dữ liệu. Dữ liệu "chôn" trong code khiến không thể chạy data-driven và dễ vỡ khi môi trường đổi. Mẹo: tách data ra file, dùng data provider.

Lỗi 6 — Over-engineering từ ngày đầu. Ngược lại với các lỗi trên: một số bạn đọc xong lại xây 8 tầng abstraction cho 10 test. Mẹo: bắt đầu với 4 tầng cơ bản, chỉ thêm abstraction khi có nhu cầu thật sự (rule of three: lặp 3 lần mới trừu tượng hóa).

Mẹo vàng: đo sức khỏe framework bằng hai chỉ số — tỷ lệ false fail (mục tiêu <2%) và thời gian sửa test khi UI đổi (nếu nhỏ, kiến trúc tốt). Nếu hai con số này xấu đi, đừng đổ lỗi cho công cụ — hãy xem lại tầng nào đang rò rỉ.

Bài tập thực hành

  • Vẽ sơ đồ tầng: Lấy dự án automation hiện tại của bạn (hoặc một dự án open-source), vẽ ra 4 tầng và đánh dấu chỗ nào đang vi phạm phân tầng (locator lọt ra ngoài, driver bị gọi thẳng...).
  • Refactor một lát cắt: Chọn 1 test viết kiểu linear, tách nó thành Page Object + Action Helper + kịch bản. So sánh số dòng phải sửa khi giả lập UI đổi locator (trước và sau refactor).
  • Xóa mọi sleep: Trong một module test, thay toàn bộ sleep cứng bằng explicit wait gói trong một class UIActions. Chạy CI 10 lần, ghi lại tỷ lệ false fail trước và sau.
  • Thiết kế contribution guide: Viết một trang quy ước ngắn cho team: locator để đâu, khi nào tạo helper, quy tắc đặt tên. Đây là bài tập tư duy QA Lead, không chỉ coder.

Tóm tắt

Kiến trúc framework automation không phải chuyện "code cho đẹp" — nó là quyết định chiến lược quyết định bộ test của bạn sống hay chết. Cốt lõi là phân tầng bốn lớp: kịch bản (Gherkin) → step definition/Page Object → action helper → driver, mỗi tầng một lý do thay đổi, mỗi tầng chỉ biết tầng dưới nó. Ba nguyên tắc bất biến: locator chỉ sống trong Page Object, mọi thao tác đi qua Action Helper (không sleep cứng), và test độc lập với nhau. Kèm theo là các module ngang thiết yếu — config, test data, reporting, CI hook.

Ba tình huống trong bài — ShopViet mất 3 tuần vì không phân tầng, PayNhanh cứu bộ test nhờ Action Helper, và startup logistics đổi được cả công cụ nhờ cô lập driver — đều nói cùng một điều: chi phí thực của automation nằm ở bảo trì, và kiến trúc chính là công cụ kiểm soát chi phí đó. Với vai trò QA Lead, hãy đầu tư vào cái khung trước khi đầu tư vào số lượng test. Một framework có kiến trúc đúng biến automation từ gánh nặng thành tài sản sinh lời qua nhiều năm.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi