Product Management
Đăng nhập
ESC

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

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

Building Automation Framework from Scratch

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

Trong suốt hành trình của khóa học, bạn đã học rời rạc từng mảnh ghép: viết locator sao cho bền, chờ đợi element bằng explicit wait, tổ chức code theo Page Object Model, chạy song song, tạo report với Allure... Nhưng có một câu hỏi lớn mà nhiều bạn QA gặp phải ở công ty thật: "Mình biết từng miếng, nhưng làm sao ghép chúng thành một cỗ máy hoàn chỉnh?"

Đây chính là khoảng cách giữa một người biết dùng công cụ và một người xây được framework. Ở nhiều team QA tại Việt Nam, tôi thấy một tình trạng phổ biến: các bạn copy một repo mẫu từ GitHub về, sửa lung tung, và sau 6 tháng không ai dám động vào nữa vì "sợ vỡ". Framework trở thành một hộp đen. Khi có người nghỉ việc, cả suite tự động hóa gần như bị bỏ hoang.

Bài này dạy bạn cách tự tay dựng một framework từ con số không, hiểu rõ từng lớp, từng quyết định thiết kế. Khi bạn tự xây được, bạn không còn phụ thuộc vào template của người khác. Bạn có thể giải thích cho sếp vì sao cấu trúc thư mục lại như vậy, vì sao cần lớp config riêng, vì sao fixture lại đặt ở đó. Đó chính là năng lực phân biệt một QA automation "thợ" với một SDET thực thụ. Chúng ta sẽ dùng ngăn xếp Python + Playwright + pytest làm ví dụ xuyên suốt, vì đây là combo hiện đại, gọn nhẹ và rất được ưa chuộng ở các startup Đông Nam Á.

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

Framework là gì và khác gì với "một mớ test case"?

Một framework tự động hóa không phải là tập hợp các file test. Nó là bộ khung có kiến trúc giúp việc viết, chạy, bảo trì và mở rộng test trở nên dễ dàng và nhất quán. Hãy hình dung framework giống như bộ khung của một ngôi nhà: móng, cột, dầm, hệ thống điện nước. Test case chỉ là đồ nội thất bạn kê vào sau.

Một framework tốt cần đảm bảo bốn tính chất: tái sử dụng (không copy-paste logic), dễ đọc (người mới vào đọc test hiểu ngay đang test gì), dễ bảo trì (đổi một chỗ, không phải sửa trăm file), và tách biệt mối quan tâm (test logic tách khỏi hạ tầng).

Các lớp (layer) của một framework

Một framework trưởng thành thường được chia thành các lớp rõ ràng, mỗi lớp một trách nhiệm:

  • Lớp test (tests/): nơi chứa các kịch bản kiểm thử. Test chỉ nên mô tả hành vi nghiệp vụ, không chứa chi tiết kỹ thuật như selector hay URL.
  • Lớp page/component (pages/): áp dụng Page Object Model, gói mọi tương tác với UI vào các class. Đây là nơi hấp thụ thay đổi giao diện.
  • Lớp config (config/): quản lý biến môi trường — base URL, timeout, thông tin đăng nhập, chọn môi trường dev/staging/prod.
  • Lớp fixtures/hooks (conftest.py): thiết lập và dọn dẹp — khởi tạo browser, đăng nhập trước, tạo dữ liệu, chụp ảnh khi fail.
  • Lớp utils/helpers (utils/): các hàm dùng chung — đọc file JSON, sinh dữ liệu ngẫu nhiên, gọi API để setup nhanh.
  • Lớp báo cáo & tích hợp (report + CI config): kết nối với Allure, GitHub Actions, Jenkins.

Nguyên tắc thiết kế cần khắc cốt ghi tâm

Có ba nguyên tắc tôi luôn nhắc học viên. Thứ nhất là DRY (Don't Repeat Yourself) — nếu bạn thấy mình copy đoạn code lần thứ hai, hãy dừng lại và trích xuất nó ra. Thứ hai là Separation of Concerns — test không được biết selector, page object không được biết assertion nghiệp vụ. Thứ ba là Fail Fast & Fail Clear — khi test rớt, thông báo lỗi phải cho biết cái gì sai chứ không phải một stack trace mù mờ.

Tình huống thực tế

Ví dụ 1 — Tiki và cái giá của việc không có lớp config

Một team QA ở một công ty thương mại điện tử lớn (tạm gọi bối cảnh giống Tiki) từng viết hơn 400 test case Selenium, nhưng hardcode base URL https://staging.tiki.vn thẳng vào trong từng test. Khi công ty tách môi trường thành staging, uatpre-prod, đội ngũ phải dùng "Find & Replace" trên 400 file để đổi URL mỗi lần chuyển môi trường. Một lần, một bạn junior quên đổi vài file, kết quả là test chạy trên staging trong khi report ghi là uat — dẫn tới một bug lọt lưới ra production, gây lỗi thanh toán trong 3 tiếng.

Bài học: Ngay từ ngày đầu, hãy tách toàn bộ giá trị môi trường ra một lớp config đọc từ biến môi trường hoặc file .env. Chuyển môi trường chỉ nên là đổi một biến duy nhất: ENV=uat pytest. Chi phí xây lớp config chỉ tốn 2 tiếng, nhưng nó đã có thể ngăn sự cố production 3 tiếng kia.

Ví dụ 2 — Startup fintech Singapore và fixture đăng nhập

Một startup fintech ở Singapore có suite gồm 120 test, và 90% trong số đó cần trạng thái "đã đăng nhập". Ban đầu mỗi test tự thực hiện flow đăng nhập qua UI: nhập email, nhập OTP giả lập, chờ dashboard load. Mỗi lần đăng nhập tốn 8 giây, nhân 120 test là gần 16 phút chỉ để... đăng nhập lặp đi lặp lại. Toàn bộ suite chạy mất 40 phút, quá chậm để tích hợp vào pipeline pull request.

Team đã tái cấu trúc: viết một fixture authenticated_page trong conftest.py, thực hiện đăng nhập một lần qua API, lưu storage_state (cookie + localStorage), rồi tái sử dụng cho mọi test. Thời gian đăng nhập tổng cộng giảm từ 16 phút xuống dưới 5 giây. Suite rút xuống còn 12 phút.

Bài học: Fixture không chỉ là chỗ khởi tạo browser. Nó là đòn bẩy hiệu năng và là nơi bạn quyết định "setup nào làm qua UI, setup nào làm qua API". Đây là triết lý framework, không phải mẹo vặt.

Ví dụ 3 — Đội ngoài (outsourcing) ở Hà Nội và bài toán "framework không ai dám sửa"

Một công ty gia công phần mềm ở Hà Nội nhận dự án tự động hóa cho khách Nhật. Bạn team lead giỏi tự dựng framework nhưng nhét tất cả vào một file base.py dài 1.500 dòng: config, page object, helper, fixture — tất tần tật. Test chạy rất tốt. Nhưng khi bạn ấy chuyển dự án, ba người mới vào không ai hiểu nổi cấu trúc. Mỗi lần thêm một test, họ mất nửa ngày dò xem hàm nằm đâu. Sau 4 tháng, khách hàng phàn nàn tốc độ giao test chậm, và cuối cùng công ty phải viết lại framework từ đầu — tốn thêm 3 tuần công.

Bài học: Framework "chạy được" chưa đủ. Nó phải dễ để người khác đọc và mở rộng. Tách lớp rõ ràng, đặt tên file theo trách nhiệm, viết một README.md ngắn hướng dẫn "muốn thêm test thì làm gì". Đầu tư vào khả năng bàn giao chính là đầu tư vào tuổi thọ của framework.

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

Giờ chúng ta cùng dựng một framework thật, từng bước, với Python + Playwright + pytest.

Bước 1 — Khởi tạo project và môi trường ảo

mkdir my-auto-framework && cd my-auto-framework
python -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install pytest playwright pytest-playwright python-dotenv allure-pytest
playwright install chromium

Tạo file requirements.txt để cố định phiên bản, giúp máy đồng nghiệp và CI chạy y hệt:

pip freeze > requirements.txt

Bước 2 — Dựng cấu trúc thư mục

Cấu trúc là bản thiết kế của ngôi nhà. Hãy tạo:

my-auto-framework/
├── config/
│   ├── __init__.py
│   └── settings.py
├── pages/
│   ├── __init__.py
│   ├── base_page.py
│   └── login_page.py
├── tests/
│   ├── __init__.py
│   └── test_login.py
├── utils/
│   ├── __init__.py
│   └── data_reader.py
├── conftest.py
├── pytest.ini
├── .env
└── requirements.txt

Bước 3 — Xây lớp config

config/settings.py đọc biến môi trường, cho phép đổi môi trường bằng một biến:

import os
from dotenv import load_dotenv

load_dotenv()

class Settings: ENV = os.getenv("ENV", "staging") BASE_URLS = { "staging": "https://staging.example.vn", "uat": "https://uat.example.vn", } BASE_URL = BASE_URLS[ENV] TIMEOUT = int(os.getenv("TIMEOUT", "10000")) USERNAME = os.getenv("TEST_USER", "qa@example.vn") PASSWORD = os.getenv("TEST_PASS", "")

settings = Settings()

File .env (nhớ thêm vào .gitignore, không commit mật khẩu):

ENV=staging
TEST_USER=qa@example.vn
TEST_PASS=SuperSecret123

Bước 4 — Lớp base page và page object

pages/base_page.py chứa hành vi chung mà mọi page kế thừa:

class BasePage:
    def __init__(self, page):
        self.page = page

def goto(self, url): self.page.goto(url)

def click(self, selector): self.page.click(selector)

def fill(self, selector, value): self.page.fill(selector, value)

pages/login_page.py — page cụ thể, gói toàn bộ selector vào một chỗ:

from pages.base_page import BasePage
from config.settings import settings

class LoginPage(BasePage): EMAIL_INPUT = "#email" PASSWORD_INPUT = "#password" SUBMIT_BTN = "button[type='submit']" WELCOME_MSG = ".dashboard-welcome"

def open(self): self.goto(f"{settings.BASE_URL}/login")

def login(self, email, password): self.fill(self.EMAIL_INPUT, email) self.fill(self.PASSWORD_INPUT, password) self.click(self.SUBMIT_BTN)

def is_logged_in(self): return self.page.locator(self.WELCOME_MSG).is_visible()

Bước 5 — Xây fixtures trong conftest.py

conftest.py là trái tim của framework — nơi setup và teardown, chụp ảnh khi fail:

import pytest
from playwright.sync_api import sync_playwright
from config.settings import settings

@pytest.fixture(scope="session") def browser(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) yield browser browser.close()

@pytest.fixture def page(browser): context = browser.new_context() page = context.new_page() page.set_default_timeout(settings.TIMEOUT) yield page context.close()

Hook chụp screenshot khi test fail

@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: page = item.funcargs.get("page") if page: page.screenshot(path=f"screenshots/{item.name}.png")

Bước 6 — Viết test đầu tiên

tests/test_login.py — chú ý test đọc rất "sạch", không dính selector:

from pages.login_page import LoginPage
from config.settings import settings

def test_login_thanh_cong(page): login = LoginPage(page) login.open() login.login(settings.USERNAME, settings.PASSWORD) assert login.is_logged_in(), "Không thấy dashboard sau khi đăng nhập"

Bước 7 — Cấu hình pytest và chạy

pytest.ini:

[pytest]
addopts = --alluredir=allure-results -v
testpaths = tests

Chạy toàn bộ suite và mở report:

ENV=staging pytest
allure serve allure-results

Đến đây bạn đã có một framework hoàn chỉnh, có lớp lang rõ ràng, đổi môi trường bằng một biến, tự chụp ảnh khi fail và xuất report đẹp. Từ nền tảng này, bạn chỉ việc thêm page object và test mới — phần khung không cần đụng lại.

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

Lỗi 1 — Nhét assertion vào page object. Nhiều bạn viết assert ngay trong login_page.py. Điều này làm page object không tái sử dụng được cho kịch bản negative. Nguyên tắc: page object trả về trạng thái, test phán xét trạng thái đó.

Lỗi 2 — Hardcode time.sleep(5). Đừng bao giờ. Playwright đã có auto-wait; nếu cần chờ điều kiện cụ thể, dùng expect() hoặc wait_for_selector. sleep cứng vừa chậm vừa flaky (bài về Wait Strategies đã bàn kỹ).

Lỗi 3 — Commit file .env chứa mật khẩu lên Git. Đây là lỗi bảo mật nghiêm trọng và rất phổ biến. Luôn thêm .env vào .gitignore ngay từ commit đầu tiên, và tạo một file .env.example (không có giá trị thật) để đồng đội biết cần khai báo biến gì.

Lỗi 4 — Xây quá nhiều lớp trừu tượng khi chưa cần. Đừng tạo AbstractBaseFactoryManager cho một dự án 20 test. Bắt đầu đơn giản, tái cấu trúc khi thực sự thấy lặp lại. Over-engineering giết chết framework nhanh không kém under-engineering.

Mẹo — Đặt "một lệnh chạy được ngay". Framework tốt là framework mà một người mới clone về, chạy pip install -r requirements.txt && pytest là xong. Nếu cần 10 bước thủ công mới chạy được, framework đã thất bại một nửa.

Mẹo — Tách setup qua API. Với mọi tiền điều kiện (đăng nhập, tạo user, tạo đơn hàng), ưu tiên gọi API thay vì click qua UI. Vừa nhanh, vừa ổn định. UI chỉ nên dùng để test đúng thứ bạn muốn test.

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

  • Dựng bộ khung: Tạo project my-auto-framework với đầy đủ cấu trúc thư mục như hướng dẫn. Đảm bảo pip install -r requirements.txt && pytest chạy được ngay sau khi clone.
  • Thêm môi trường thứ ba: Bổ sung môi trường prod vào settings.py và verify việc chuyển môi trường chỉ cần đổi biến ENV, không sửa bất kỳ file test nào.
  • Viết fixture đăng nhập tái sử dụng: Tạo fixture authenticated_page thực hiện đăng nhập một lần và lưu storage_state, rồi viết hai test dùng lại nó. Đo thời gian trước và sau để thấy sự khác biệt.
  • Thêm page object thứ hai: Chọn một trang thật (ví dụ trang tìm kiếm sản phẩm) và viết search_page.py cùng một test tương ứng, giữ nguyên nguyên tắc "test không chứa selector".
  • Viết README bàn giao: Viết một README.md 15 dòng hướng dẫn "làm sao để thêm một test mới". Đây là bài tập rèn tư duy bàn giao — thứ phân biệt SDET giỏi với người viết code cho riêng mình.

Tóm tắt

Xây framework từ đầu không phải là viết thật nhiều code, mà là ra quyết định kiến trúc đúng: tách config khỏi test, gói UI vào page object, dồn setup vào fixture, và giữ mọi thứ đủ đơn giản để người sau đọc hiểu. Chúng ta đã đi qua bảy bước cụ thể với Python + Playwright + pytest, từ khởi tạo project đến chạy được suite có report Allure và tự chụp ảnh khi fail.

Ba bài học từ Tiki, startup fintech Singapore và đội gia công Hà Nội cho thấy cùng một chân lý: framework chạy được chỉ là điều kiện cần; framework dễ đổi môi trường, dễ tái sử dụng và dễ bàn giao mới là điều kiện đủ. Hãy bắt đầu đơn giản, tuân thủ DRY và Separation of Concerns, và tái cấu trúc khi thực sự cần. Khi bạn tự tay dựng được cỗ máy này, bạn không còn là người dùng framework của người khác — bạn là người kiến tạo. Đó chính là bước chuyển mình quan trọng nhất trên con đường trở thành SDET.

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