Mở đầu — vì sao bài này quan trọng
Bạn đã học qua Selenium, Page Object Model, wait strategy, data-driven... Từng mảnh ghép ấy giống như bạn biết lát gạch, đi dây điện, xây tường. Nhưng biết từng kỹ thuật riêng lẻ chưa đủ để "xây một ngôi nhà" — tức là một test framework hoàn chỉnh mà cả team có thể dùng chung, mở rộng được, và không sụp đổ sau ba tháng.
Đây chính là ranh giới phân biệt một QA biết viết script với một SDET (Software Development Engineer in Test) thực thụ. Ở nhiều công ty Việt Nam như FPT Software, KMS Technology, hay các startup fintech ở TP.HCM, khi phỏng vấn vị trí Automation/SDET mức middle trở lên, câu hỏi gần như luôn xuất hiện là: "Nếu được giao xây framework test từ đầu, em sẽ thiết kế những component nào và tại sao?". Người trả lời được câu này một cách có hệ thống thường nhận offer cao hơn hẳn.
Trong bài này, chúng ta không học thêm một tool mới. Chúng ta học cách lắp ghép mọi thứ đã biết thành một kiến trúc — nghĩ như một kiến trúc sư phần mềm chứ không phải người viết script. Đây là bài "xương sống" của cả khóa: hiểu nó rồi thì các bài về CI/CD, reporting, parallel execution sau này chỉ là những khối lắp thêm vào bộ khung bạn dựng hôm nay.
Khái niệm cốt lõi
Framework là gì và không là gì
Một test framework không phải là "một đống file test gộp lại". Nó là một tập hợp các quy ước (convention), thư viện tiện ích (utilities), lớp trừu tượng (abstraction) và cấu hình (configuration) giúp cho việc viết test mới trở nên nhanh, việc chạy test trở nên nhất quán, và việc bảo trì trở nên rẻ.
Nói cách khác, thước đo của một framework tốt không phải là "chạy được 100 test case", mà là: khi test case thứ 101 cần thêm vào, người viết mất bao nhiêu công? Nếu mỗi test mới đều phải copy-paste 50 dòng setup, framework của bạn đã hỏng.
Kiến trúc phân tầng (layered architecture)
Framework tốt được chia thành các tầng, mỗi tầng chỉ nói chuyện với tầng ngay dưới nó. Đây là nguyên tắc quan trọng nhất — separation of concerns (tách bạch mối quan tâm):
┌─────────────────────────────────────┐
│ Test Layer (test cases / specs) │ ← "WHAT to test" — nghiệp vụ
├─────────────────────────────────────┤
│ Business/Flow Layer (workflows) │ ← các luồng nghiệp vụ tái sử dụng
├─────────────────────────────────────┤
│ Page Object Layer (POM) │ ← đại diện màn hình/API endpoint
├─────────────────────────────────────┤
│ Core/Driver Layer (utilities) │ ← WebDriver, HTTP client, waits
├─────────────────────────────────────┤
│ Config & Data Layer │ ← environment, test data
└─────────────────────────────────────┘
Tầng test case chỉ nên chứa ngôn ngữ nghiệp vụ: "đăng nhập với tài khoản hợp lệ, kỳ vọng vào được dashboard". Nó không được biết driver.findElement(By.id(...)) là gì — chi tiết đó nằm ở tầng dưới. Nhờ vậy, khi UI đổi locator, bạn sửa một chỗ ở Page Object, còn 200 test case phía trên không hề hấn gì.
Cấu trúc thư mục điển hình
Đây là bộ khung thư mục mà bạn có thể mang đi áp dụng cho hầu hết dự án (ví dụ với Java + Selenium, nhưng triết lý áp dụng cho mọi ngôn ngữ):
my-framework/
├── config/ # Cấu hình theo môi trường
│ ├── dev.json
│ ├── staging.json
│ └── prod.json
├── src/
│ ├── core/ # Nền tảng: DriverFactory, BasePage, waits
│ │ ├── DriverFactory.java
│ │ ├── ConfigReader.java
│ │ └── BaseTest.java
│ ├── pages/ # Page Objects
│ │ ├── LoginPage.java
│ │ └── DashboardPage.java
│ ├── flows/ # Business workflows tái sử dụng
│ │ └── CheckoutFlow.java
│ └── utils/ # Tiện ích: logger, screenshot, API client
│ ├── ScreenshotUtil.java
│ └── ApiClient.java
├── tests/ # Test cases thật sự
│ ├── login/
│ └── checkout/
├── testdata/ # Dữ liệu test (CSV, JSON, Excel)
├── reports/ # Nơi xuất báo cáo
├── testng.xml / suite files # Cấu hình bộ test
└── pom.xml / build.gradle # Quản lý dependency
Các component bắt buộc phải có
Một framework "đủ dùng" cho môi trường thật cần tối thiểu 6 component. Hãy nhớ chúng như một checklist:
- Configuration Management — đọc config theo môi trường. Không được hard-code URL, username, password vào code. Chạy
dev,staging,prodchỉ khác nhau một biến môi trườngENV=staging.
- Driver/Client Factory — nơi duy nhất khởi tạo WebDriver (hoặc HTTP client). Ai cần driver thì xin từ factory, không tự
new. Nhờ vậy đổi từ Chrome sang Firefox, hay bật headless, chỉ sửa một chỗ.
- Base classes —
BaseTestlo phần setup/teardown (mở/đóng browser, chụp ảnh khi fail),BasePagechứa các phương thức chung (click, type, waitVisible) để mọi Page Object kế thừa.
- Test Data Management — tách dữ liệu ra khỏi logic. Test đọc dữ liệu từ file/generator, không nhét thẳng vào code.
- Logging & Reporting — ghi log từng bước và xuất báo cáo có ảnh chụp khi lỗi. Khi test fail lúc 2 giờ sáng trên CI, log là thứ duy nhất giúp bạn hiểu chuyện gì đã xảy ra.
- Utilities — screenshot, đọc/ghi file, tạo dữ liệu ngẫu nhiên, retry mechanism.
Nguyên tắc thiết kế cần thấm
- DRY (Don't Repeat Yourself): thấy code lặp lần thứ ba là dấu hiệu cần tách thành utility hoặc method chung.
- Single Responsibility: mỗi class làm một việc.
LoginPagechỉ biết về trang login, không tự đọc config hay tự gửi report. - Fail loud, fail clear: khi lỗi, thông báo phải nói rõ cái gì sai, ở đâu, kèm ảnh chụp — chứ không phải một
NullPointerExceptionvô hồn. - Config over code: mọi thứ có thể thay đổi (URL, timeout, trình duyệt) nên nằm ở config, không nằm trong code.
Tình huống thực tế
Ví dụ 1 — Startup fintech ở TP.HCM và "framework mì ăn liền"
Một startup ví điện tử tại quận 1 (gọi là công ty A) có team QA 4 người. Ban đầu, để kịp deadline, họ viết test theo kiểu mỗi người một file, mỗi file tự mở browser, tự nhập URL https://staging.congtyA.vn cứng trong code. Sau 5 tháng, họ có 340 test case.
Rồi công ty tách môi trường: thêm pre-prod và đổi domain staging. Kết quả: họ phải sửa hơn 300 chỗ chứa URL cứng, mất gần 2 ngày công, và vẫn sót vài chỗ khiến test chạy nhầm môi trường — suýt tạo giao dịch thật trên prod.
Bài học rút ra: đây chính là cái giá của việc thiếu Configuration Management. Nếu ngay từ đầu họ có một config/staging.json và một ConfigReader, việc đổi domain chỉ là sửa một dòng. Sau sự cố, họ dành 3 ngày refactor thành kiến trúc phân tầng đúng chuẩn — và lần đổi môi trường tiếp theo chỉ mất 10 phút.
Ví dụ 2 — KMS Technology và sức mạnh của Base classes
Một team tại một công ty outsourcing lớn ở TP.HCM nhận dự án cho khách hàng Mỹ. Yêu cầu: mỗi khi test fail, phải tự động chụp màn hình đính kèm vào report để khách bên kia bán cầu debug được mà không cần hỏi lại.
Nhờ đã thiết kế BaseTest với một hook @AfterMethod kiểm tra kết quả — nếu FAILED thì gọi ScreenshotUtil.capture() — họ chỉ cần viết logic chụp ảnh đúng một lần. Toàn bộ 500+ test case tự động có ảnh chụp khi lỗi mà không dòng test nào phải viết thêm gì.
Con số cụ thể: thời gian trung bình để khách xác định nguyên nhân một test fail giảm từ khoảng 40 phút (phải hỏi qua email, chờ múi giờ) xuống dưới 5 phút vì ảnh đã nằm sẵn trong report.
Bài học rút ra: một quyết định kiến trúc nhỏ đặt đúng chỗ (hook trong Base class) nhân giá trị lên toàn bộ suite. Đây là lý do tầng core quan trọng hơn nhiều so với vẻ ngoài "ít code" của nó.
Ví dụ 3 — Framework "over-engineered" của một bạn junior
Một bạn SDET mới lên middle, quá hào hứng, tự xây framework với 8 tầng trừu tượng, factory cho cả factory, và một hệ thống config đọc từ 4 nguồn khác nhau. Kết quả: đồng đội mất 2 tuần chỉ để hiểu cách thêm một test case đơn giản, và cuối cùng ai cũng né không dám đụng vào.
Bài học rút ra: kiến trúc tốt là kiến trúc vừa đủ. Đừng thêm abstraction cho vấn đề chưa xảy ra. Nguyên tắc YAGNI (You Aren't Gonna Need It) rất quan trọng: bắt đầu đơn giản, chỉ tách tầng khi thực sự thấy code lặp hoặc thấy đau. Một framework mà người mới đọc 30 phút là viết được test đầu tiên — đó mới là framework thành công.
Hướng dẫn từng bước
Hãy cùng dựng bộ khung tối thiểu nhưng đúng chuẩn, theo trình tự:
Bước 1 — Dựng cấu trúc thư mục và quản lý dependency. Tạo cây thư mục như phần trên, khởi tạo pom.xml (Java) hoặc requirements.txt/package.json. Đây là bộ xương, chưa có thịt.
Bước 2 — Viết Config layer trước tiên. Tạo config/dev.json và config/staging.json với các key như baseUrl, browser, timeout. Viết ConfigReader đọc file dựa trên biến môi trường ENV. Nguyên tắc: không một dòng code nào khác được phép biết URL nằm ở đâu ngoài ConfigReader.
// config/staging.json
{
"baseUrl": "https://staging.myapp.vn",
"browser": "chrome",
"timeoutSeconds": 15
}
Bước 3 — Xây Driver Factory. Một class duy nhất chịu trách nhiệm tạo WebDriver dựa trên config. Trả về driver đã cấu hình sẵn (headless, kích thước cửa sổ, implicit wait). Không nơi nào khác được gọi new ChromeDriver().
Bước 4 — Tạo Base classes. BaseTest với @BeforeMethod xin driver từ factory, @AfterMethod đóng driver và chụp ảnh nếu fail. BasePage nhận driver qua constructor và cung cấp click(), type(), waitVisible() — dùng explicit wait bên trong.
Bước 5 — Viết một Page Object mẫu. Ví dụ LoginPage kế thừa BasePage, khai báo locator và các hành động nghiệp vụ như login(user, pass). Page Object không chứa assertion — assertion thuộc về test.
Bước 6 — Viết test case đầu tiên chỉ bằng ngôn ngữ nghiệp vụ. Test đọc như văn xuôi: loginPage.login(validUser); assertThat(dashboardPage.isLoaded()).isTrue();. Nếu test của bạn còn thấy By.id, hãy đẩy nó xuống Page Object.
Bước 7 — Cắm Logging & Reporting. Thêm logger (Log4j/SLF4J) và một reporter (sẽ học sâu ở bài Allure). Đảm bảo mỗi bước quan trọng có log, mỗi lỗi có ảnh.
Bước 8 — Tạo file suite và chạy thử theo môi trường. Chạy ENV=staging và ENV=dev để xác nhận cùng một bộ test chạy được trên hai môi trường mà không sửa code. Nếu làm được điều này — bạn đã có một framework thật sự.
Lỗi thường gặp & mẹo
- Hard-code mọi thứ. URL, credential, timeout nằm rải rác trong code là "tội tổ tông" phổ biến nhất. Mẹo: quy tắc "không có string ma thuật" — mọi giá trị cấu hình phải đến từ config.
- Page Object phình to chứa cả assertion và test logic. Page Object chỉ mô tả màn hình làm được gì, không phán xét đúng hay sai. Giữ assertion ở tầng test.
- Setup/teardown copy-paste trong từng test. Nếu thấy
driver = new ...trong nhiều file test, đó là dấu hiệu bạn chưa có Base class đúng nghĩa.
- Over-engineering ngay từ đầu. Đừng xây 6 tầng abstraction cho 10 test case. Bắt đầu với 3 tầng (config, page, test), tách thêm khi thấy đau. Nhớ YAGNI.
- Quên xử lý credential nhạy cảm. Đừng commit password prod lên Git. Dùng biến môi trường hoặc secret manager, và đưa
config/prod.jsonvào.gitignorenếu chứa thông tin nhạy cảm.
- Không có retry/screenshot cho flaky. Môi trường thật luôn có flaky. Thiết kế sẵn chỗ để cắm retry và ảnh chụp — dù bài chuyên sâu về flaky sẽ học sau, framework của bạn phải "chừa cửa" cho nó.
- Mẹo vàng: trước khi viết framework, hãy tự hỏi "đồng đội mới vào mất bao lâu để viết test đầu tiên?". Câu trả lời lý tưởng là dưới 1 giờ. Kiến trúc phục vụ con người, không phải để khoe kỹ thuật.
Bài tập thực hành
- Vẽ kiến trúc. Chọn một website quen thuộc (ví dụ trang đăng nhập của một ví điện tử Việt Nam). Vẽ ra giấy sơ đồ 5 tầng và liệt kê những gì thuộc mỗi tầng cho luồng "đăng nhập".
- Dựng bộ khung tối thiểu. Tạo cấu trúc thư mục như trong bài. Viết
ConfigReaderđọcdev.jsonvàstaging.json, in rabaseUrltương ứng với biếnENV. Xác nhận đổiENVlà đổi URL mà không sửa code.
- Refactor một script cũ. Lấy một test script bạn từng viết theo kiểu "tất cả trong một file". Tách nó thành Config + DriverFactory + BasePage + LoginPage + TestCase. Ghi lại: sau khi tách, test case còn bao nhiêu dòng và đọc có giống văn xuôi nghiệp vụ không?
- Thử nghiệm Base class. Thêm hook
@AfterMethodtự chụp màn hình khi test fail. Cố tình cho một test fail và kiểm tra ảnh có được sinh ra trongreports/không.
- Câu hỏi phỏng vấn. Viết ra câu trả lời 3–4 câu cho: "Framework của bạn có những component gì và mỗi component giải quyết vấn đề nào?". Luyện nói trôi chảy — đây là câu hỏi bạn gần như chắc chắn gặp khi phỏng vấn SDET.
Tóm tắt
Một test framework tốt không phải là nơi chứa nhiều test, mà là một kiến trúc phân tầng giúp viết test nhanh, chạy nhất quán và bảo trì rẻ. Xương sống của nó gồm 6 component: Configuration Management, Driver/Client Factory, Base classes, Test Data Management, Logging & Reporting, và Utilities — tất cả tuân theo nguyên tắc tách bạch mối quan tâm, DRY và config-over-code.
Ba bài học từ thực tế: thiếu config layer khiến việc đổi môi trường thành ác mộng (công ty A); một hook đặt đúng chỗ trong Base class nhân giá trị lên toàn suite (KMS); và over-engineering giết chết năng suất team (bạn junior). Kim chỉ nam cuối cùng: kiến trúc phục vụ con người — thước đo thành công là đồng đội mới viết được test đầu tiên trong dưới một giờ.
Nắm vững bộ khung này, bạn đã sẵn sàng cho các bài tiếp theo — CI/CD, parallel execution, reporting với Allure — vì tất cả chỉ là những khối lắp thêm vào kiến trúc bạn vừa dựng hôm nay.