Đang tải...

Lửng Lọc Lõi

Đeo kính để soi Bug cho rõ, nhíu mày để nhắc Dev sửa cho kỹ. Với tôi, 'chạy được' thôi là chưa đủ! 🧐💻


0

Từ Manual Đến Selenium Automation: Lộ Trình Cho Tester 1 Năm Kinh Nghiệm

Lửng Lọc Lõi
Lửng Lọc Lõi

5 tháng trước · 17 phút đọc

Tại sao tester 1 năm nên bắt đầu học Selenium ngay bây giờ

Mình nhớ lần đầu nghe từ "automation testing", cảm giác đó giống như ai đó nói bạn phải học lập trình từ đầu. Sợ. Ngại. Nghĩ "chắc không phải dành cho mình". Mình đã nghĩ vậy suốt 6 tháng trước khi buộc phải học vì team yêu cầu.

Sự thật? Automation testing không đòi hỏi bạn phải là developer. Nó đòi hỏi bạn hiểu test case - thứ mà sau 1 năm làm manual, bạn đã có đủ.

Thị trường tuyển dụng QA tại Việt Nam đang thay đổi rõ. Phần lớn tin tuyển dụng tester hiện tại yêu cầu hoặc ưu tiên ứng viên có kỹ năng automation. Nếu bạn đang ở mức manual thuần sau 1-2 năm, lương của bạn đang bị chặn trần - không phải vì bạn không giỏi, mà vì thị trường đang trả thêm cho automation skill.

Bài này dành cho bạn đang làm manual testing được khoảng 1 năm, biết viết test case ổn, nhưng chưa động vào code. Mình sẽ đi từng bước, giải thích tại sao từng bước quan trọng, và dùng ví dụ thực tế từ các app e-commerce và banking - loại dự án phổ biến nhất ở Việt Nam hiện nay.

343548 Automation không thay manual - nó giải phóng bạn khỏi việc click lặp đi lặp lại

Một điều cần nói thẳng: lộ trình này mất 3-6 tháng nếu bạn học đều đặn 1-2 giờ/ngày. Không phải 2 tuần. Không phải 1 tháng. Ai hứa với bạn nhanh hơn thế thì đang nói dối. Nhưng 3-6 tháng đó đổi lại là mức lương cao hơn đáng kể và cơ hội việc làm rộng hơn nhiều.


Giai đoạn 1: Hiểu Selenium trước khi viết code (tuần 1-2)

Sai lầm phổ biến nhất mình thấy ở người mới học automation: mở tutorial, copy code, chạy thử, không hiểu tại sao nó chạy - rồi gặp lỗi đầu tiên là bỏ cuộc.

Selenium là gì trong 1 câu: Đây là thư viện cho phép bạn điều khiển trình duyệt bằng code, giống như bạn đang dùng tay click nhưng máy tính làm thay.

Trước khi gõ dòng code đầu tiên, bạn cần hiểu 3 khái niệm nền:

1. WebDriver - là "tay" của Selenium, dùng để giao tiếp với trình duyệt Chrome/Firefox. Bạn cần tải ChromeDriver tương ứng với version Chrome đang dùng.

2. Locator - cách Selenium tìm element trên trang web. Có 8 loại locator, nhưng thực tế bạn chỉ cần thành thạo 3: id, xpath, và css selector. Phần còn lại học sau cũng được.

3. Actions - những gì bạn làm với element sau khi tìm được: click, nhập text, lấy text để verify.

343549 3 khái niệm này là nền tảng - hiểu vững trước khi code

Sau tuần đầu, mục tiêu của bạn không phải là viết được automation test. Mục tiêu là cài được môi trường và chạy được 1 script đơn giản mà bạn hiểu từng dòng.

Setup môi trường cụ thể:

  • Cài Python (mình khuyên dùng Python thay Java vì syntax đơn giản hơn cho người mới)
  • Cài Selenium: pip install selenium
  • Tải ChromeDriver tương ứng version Chrome
  • IDE: VS Code, cài thêm Python extension

Script đầu tiên nên là gì? Mở trang Shopee, tìm ô search, nhập "áo phông", click tìm kiếm, kiểm tra kết quả có xuất hiện không. 10 dòng code. Không hơn.

# Script đầu tiên - tìm kiếm trên Shopee
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time

# Bước 1: Mở trình duyệt
driver = webdriver.Chrome()
driver.get("https://shopee.vn")

# Bước 2: Tìm ô search và nhập từ khóa
search_box = driver.find_element(By.XPATH, '//input[@placeholder]')
search_box.send_keys("áo phông")
search_box.send_keys(Keys.RETURN)

# Bước 3: Chờ kết quả load (tạm dùng sleep, sau này thay bằng wait)
time.sleep(3)

# Bước 4: Verify kết quả có xuất hiện
results = driver.find_elements(By.CLASS_NAME, "shopee-search-item-result")
print(f"Tìm thấy {len(results)} sản phẩm")

driver.quit()

Chạy được đoạn này rồi, bạn đã vượt qua rào cản lớn nhất. Phần còn lại chỉ là mở rộng thêm.


Giai đoạn 2: Viết test case thực tế - ví dụ luồng đăng nhập (tuần 3-4)

Bạn đã chạy được script đơn giản. Bây giờ mới đến phần quan trọng: chuyển test case manual bạn đang làm hàng ngày thành automation script.

Đây là điểm mà nền tảng 1 năm manual của bạn phát huy giá trị. Bạn đã biết test case đăng nhập cần cover những gì: happy path, sai password, tài khoản bị khóa, ô trống, ký tự đặc biệt. Developer mới học automation không có sẵn cái nhìn đó.

Lấy ví dụ tính năng đăng nhập của một app banking - loại dự án mình thấy nhiều nhất ở các công ty fintech tại Hà Nội và TP.HCM:

# Test case: Đăng nhập thất bại với password sai
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class TestLogin:
    def setup_method(self):
        # Khởi tạo driver trước mỗi test
        self.driver = webdriver.Chrome()
        self.driver.get("https://demo-banking.com/login")
        # Dùng WebDriverWait thay time.sleep - tốt hơn nhiều
        self.wait = WebDriverWait(self.driver, 10)

    def teardown_method(self):
        # Đóng browser sau mỗi test - bắt buộc
        self.driver.quit()

    def test_login_wrong_password(self):
        """TC-001: Đăng nhập với password sai hiển thị thông báo lỗi"""
        # Bước 1: Nhập username hợp lệ
        username_field = self.wait.until(
            EC.presence_of_element_located((By.ID, "username"))
        )
        username_field.send_keys("[email protected]")

        # Bước 2: Nhập password SAI
        password_field = self.driver.find_element(By.ID, "password")
        password_field.send_keys("SaiPassword123")

        # Bước 3: Click đăng nhập
        self.driver.find_element(By.ID, "login-btn").click()

        # Bước 4: Verify thông báo lỗi xuất hiện
        error_msg = self.wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, "error-message"))
        )
        assert "Sai tên đăng nhập hoặc mật khẩu" in error_msg.text
        print("✓ TC-001 PASSED: Hiển thị đúng thông báo lỗi")

343550 Chú ý: WebDriverWait thay thế time.sleep - đây là thói quen phân biệt script tốt và xấu

Bạn thấy điểm khác biệt so với script đầu tiên không? Có 3 cải tiến quan trọng:

  1. Dùng WebDriverWait thay time.sleep - script sẽ không bị fail khi mạng chậm hơn bình thường 0.5 giây
  2. setup_methodteardown_method - đảm bảo mỗi test case chạy độc lập, không ảnh hưởng nhau
  3. Assert có message rõ ràng - khi test fail, bạn biết ngay fail ở đâu

Mình từng bỏ qua teardown_method vì nghĩ "chạy local test thôi". Kết quả là chạy 50 test case mở 50 cửa sổ Chrome, máy treo. Bài học đắt giá.


Giai đoạn 3: Page Object Model - cách tổ chức code cho tester không phải developer (tuần 5-8)

Page Object Model (POM) - nghe tên fancy nhưng ý tưởng rất đơn giản: tách phần "tìm element" ra khỏi phần "logic test".

Tại sao cần làm vậy? Hãy nghĩ đến tình huống này: bạn có 20 test case dùng ô nhập username. Dev đổi id="username" thành id="user-input". Không có POM, bạn phải sửa 20 chỗ. Có POM, bạn chỉ sửa 1 chỗ.

Đây là cách implement POM cho trang đăng nhập banking:

# pages/login_page.py - định nghĩa elements và actions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LoginPage:
    # Locators - tất cả element ở đây
    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON = (By.ID, "login-btn")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
    DASHBOARD_HEADER = (By.ID, "dashboard-header")

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def enter_username(self, username):
        """Nhập username vào ô input"""
        field = self.wait.until(EC.presence_of_element_located(self.USERNAME_INPUT))
        field.clear()  # Xóa trước khi nhập - hay bị quên
        field.send_keys(username)

    def enter_password(self, password):
        """Nhập password vào ô input"""
        field = self.driver.find_element(*self.PASSWORD_INPUT)
        field.clear()
        field.send_keys(password)

    def click_login(self):
        """Click nút đăng nhập"""
        self.driver.find_element(*self.LOGIN_BUTTON).click()

    def login(self, username, password):
        """Thực hiện toàn bộ luồng đăng nhập"""
        self.enter_username(username)
        self.enter_password(password)
        self.click_login()

    def get_error_message(self):
        """Lấy text thông báo lỗi"""
        return self.wait.until(
            EC.presence_of_element_located(self.ERROR_MESSAGE)
        ).text

    def is_login_successful(self):
        """Kiểm tra đăng nhập thành công"""
        try:
            self.wait.until(EC.presence_of_element_located(self.DASHBOARD_HEADER))
            return True
        except:
            return False
# tests/test_login.py - file test sạch, dễ đọc
from pages.login_page import LoginPage

class TestLogin:
    def test_login_success(self, driver):
        login = LoginPage(driver)
        login.login("[email protected]", "Password123!")
        assert login.is_login_successful()

    def test_login_wrong_password(self, driver):
        login = LoginPage(driver)
        login.login("[email protected]", "SaiPassword")
        assert "Sai tên đăng nhập" in login.get_error_message()

    def test_login_empty_fields(self, driver):
        login = LoginPage(driver)
        login.click_login()  # Không nhập gì
        assert "Vui lòng nhập" in login.get_error_message()

343551 POM tách biệt rõ ràng: tầng pages định nghĩa element, tầng tests chứa logic kiểm tra

So sánh file test_login.py với và không có POM: bản có POM đọc như một đoạn văn mô tả test case. Bất kỳ tester nào cũng hiểu ngay dù không biết Selenium. Đó là dấu hiệu của automation test được viết tốt.

Thấy khó ở phần cấu trúc folder này bình thường lắm. Mình mất gần 2 tuần mới quen với việc tổ chức code theo kiểu này. Bạn có thể tải template cấu trúc project Selenium POM để bắt đầu nhanh hơn: Tải Selenium POM Template


Giai đoạn 4: Test luồng phức tạp - ví dụ giỏ hàng e-commerce (tuần 9-12)

Đăng nhập là bước khởi động. Tester thực sự trị giá khi test được các luồng nghiệp vụ phức tạp: thêm sản phẩm vào giỏ, áp mã giảm giá, thanh toán. Đây là loại test mà manual testing tốn 45-60 phút/lần regression - automation chạy hết trong 3-5 phút.

Luồng e-commerce điển hình cần test:

Scenario: Mua hàng thành công với mã giảm giá

  1. Đăng nhập
  2. Tìm kiếm sản phẩm
  3. Chọn size/màu, thêm vào giỏ
  4. Vào giỏ hàng, verify số lượng
  5. Nhập mã giảm giá, verify giá thay đổi
  6. Chọn địa chỉ giao hàng
  7. Chọn phương thức thanh toán
  8. Xác nhận đơn hàng
  9. Verify trang cảm ơn + mã đơn hàng

Mỗi bước có thể fail vì lý do khác nhau. Đây là lý do tại sao explicit wait quan trọng hơn bao giờ hết ở luồng phức tạp.

# pages/cart_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from pages.base_page import BasePage

class CartPage(BasePage):
    ADD_TO_CART_BTN = (By.ID, "add-to-cart")
    CART_COUNT = (By.CLASS_NAME, "cart-count")
    COUPON_INPUT = (By.ID, "coupon-code")
    APPLY_COUPON_BTN = (By.ID, "apply-coupon")
    TOTAL_PRICE = (By.ID, "total-price")
    DISCOUNT_AMOUNT = (By.CLASS_NAME, "discount-row")

    def add_to_cart(self):
        """Thêm sản phẩm vào giỏ và chờ cart count cập nhật"""
        current_count = self.get_cart_count()
        self.click(self.ADD_TO_CART_BTN)
        # Chờ cart count tăng lên - không dùng sleep
        self.wait.until(
            lambda d: self.get_cart_count() > current_count
        )

    def apply_coupon(self, coupon_code):
        """Áp mã giảm giá và verify discount xuất hiện"""
        self.enter_text(self.COUPON_INPUT, coupon_code)
        self.click(self.APPLY_COUPON_BTN)
        # Chờ dòng discount xuất hiện trong UI
        self.wait.until(EC.visibility_of_element_located(self.DISCOUNT_AMOUNT))

    def get_total_price(self):
        """Lấy tổng tiền hiển thị trên trang"""
        price_text = self.get_text(self.TOTAL_PRICE)
        # Chuyển '1.250.000 đ' thành số nguyên 1250000
        return int(price_text.replace('.',"").replace(' đ',""))

    def get_cart_count(self):
        try:
            return int(self.get_text(self.CART_COUNT))
        except:
            return 0

343552 Luồng giỏ hàng - nơi automation tiết kiệm hàng giờ regression testing mỗi sprint

Một điểm nhiều tester hay bỏ qua khi test luồng phức tạp: test data. Mã giảm giá hết hạn, sản phẩm hết hàng, địa chỉ ship không hợp lệ - tất cả đều có thể làm test fail vì lý do không phải bug.

Giải pháp: tạo file test_data.py riêng chứa tất cả data, đặt expiry date xa (2030), dùng tài khoản test được team backend tạo riêng cho automation. Đừng dùng tài khoản thật của nhân viên - mình từng vô tình mua đơn hàng thật khi test production vì không kiểm tra kỹ environment.


Giai đoạn 5: Tích hợp pytest và chạy CI cơ bản (tuần 13-16)

Bạn đã có test script. Vấn đề: chạy script bằng tay mỗi sáng không phải automation thực sự. Automation thực sự là chạy tự động mà không cần bạn ngồi đó.

pytest là framework tổ chức và chạy test mà mình khuyên dùng nhất cho Python. Không phải vì phức tạp, mà vì đơn giản. Và thị trường Việt Nam đang dùng nhiều.

Cấu hình conftest.py - file quan trọng nhất trong project pytest:

# conftest.py - đặt ở root folder project
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture(scope="function")
def driver():
    """Fixture tạo và đóng browser cho mỗi test"""
    options = Options()
    # Chạy headless khi CI/CD - không mở browser thật
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")

    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(10)
    driver.maximize_window()

    yield driver  # Trả driver cho test
    driver.quit()  # Chạy sau khi test xong

@pytest.fixture(scope="function")
def logged_in_driver(driver):
    """Fixture cho test cần đăng nhập sẵn"""
    from pages.login_page import LoginPage
    login = LoginPage(driver)
    login.login("[email protected]", "TestPass123!")
    return driver
# Chạy toàn bộ test suite
pytest tests/ -v

# Chạy test theo tag (marker)
pytest tests/ -m "smoke" -v

# Tạo HTML report
pytest tests/ --html=reports/report.html

343553 CI pipeline tự động chạy test mỗi khi có code mới - đây là điểm khác biệt so với chạy thủ công

Bước cuối là tích hợp với GitHub Actions - công cụ CI/CD miễn phí của GitHub. File .github/workflows/test.yml:

# Tự động chạy test mỗi khi có Pull Request
name: Selenium Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Cài Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Cài thư viện
        run: pip install -r requirements.txt
      - name: Chạy test
        run: pytest tests/ -v --html=reports/report.html
      - name: Upload report
        uses: actions/upload-artifact@v3
        with:
          name: test-report
          path: reports/report.html

Khi dev tạo Pull Request, pipeline này tự động chạy và báo pass/fail trước khi merge. Đây là thứ team lead muốn thấy từ automation tester. Bạn không chỉ viết script - bạn tạo ra safety net cho cả team.


Những lỗi phổ biến khiến tester mãi không lên được level automation

Mình quan sát nhiều bạn học Selenium rồi bỏ giữa chừng. Không phải vì khó, mà vì mắc một trong số những bẫy dưới đây.

Bẫy 1: Copy-paste code không hiểu

Bạn Google "Selenium login test python", copy về, chạy được, xong. Một tuần sau gặp lỗi mới, không biết sửa vì không hiểu code mình đang dùng. Nguyên tắc: nếu bạn không giải thích được dòng code cho người khác, bạn chưa thật sự học nó.

Bẫy 2: Dùng time.sleep khắp nơi

Test chạy được local nhưng fail liên tục trên CI vì server CI chậm hơn máy bạn 0.5 giây. Giải pháp duy nhất: WebDriverWait với expected_conditions như đã nói ở trên.

Bẫy 3: Không quản lý test data

Test data (mã giảm giá, tài khoản test, sản phẩm mẫu) thay đổi mà không báo trước. Script fail vì data chết, không phải vì bug. Dành 1 buổi tạo file test_data.py và thống nhất với team backend về test data.

Bẫy 4: Bỏ qua việc đọc error message

Khi test fail, nhiều bạn chỉ nhìn cái dòng đỏ cuối cùng. NoSuchElementException có thể nghĩa là: element không tồn tại, chưa load kịp, hoặc locator sai. Đọc full stack trace - thông tin ở đó đủ để debug trong 90% trường hợp.

343554 Hiểu error message đúng cách tiết kiệm 80% thời gian debug

Bẫy 5: Không viết test độc lập

Test A phải chạy trước test B mới pass. Đây là dấu hiệu script phụ thuộc lẫn nhau. Trên CI chạy song song, test B sẽ fail ngẫu nhiên. Mỗi test case phải có thể chạy độc lập, bắt đầu từ trạng thái sạch.

Nếu bạn đang tự học JavaScript thay vì Python cho automation (một số team dùng WebdriverIO), khóa Lập Trình JavaScript Cơ Bản của F8 là điểm xuất phát hợp lý - miễn phí và đi từ nền tảng thay vì ném thẳng vào syntax phức tạp.

Tóm lại, để bắt đầu lộ trình automation:

  1. Tuần 1-2: Cài môi trường, chạy script đầu tiên (tìm kiếm Shopee)
  2. Tuần 3-4: Chuyển test case manual quen thuộc nhất sang automation
  3. Tuần 5-8: Học POM, tổ chức code cho dễ bảo trì
  4. Tuần 9-12: Test luồng phức tạp (cart, checkout, payment)
  5. Tuần 13-16: Tích hợp pytest + CI/CD cơ bản

Bước cuối: tải checklist lộ trình chi tiết theo tuần tại đây: Tải Checklist Lộ Trình Selenium 16 Tuần

Test kỹ 1 lần tốt hơn fix lỗi production nhiều lần. Automation giúp bạn làm điều đó mà không cần ngồi click tay mỗi ngày. Comment bên dưới nếu bạn đang mắc kẹt ở bước nào - mình trả lời từng bạn.