Đ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

Automation Với Selenium: Xử Lý Popup, Ajax Và Multi-Tab Cho Fresher

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

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

Từ manual testing sang automation: bắt đầu từ đâu?

Bạn đang test tay một e-commerce. Mỗi sprint, dev deploy tính năng mới - bạn lại phải click qua 50 test case, verify popup khuyến mãi, kiểm tra ajax load sản phẩm, đóng tab thanh toán... Sau 3 tháng, cảm giác bắt đầu mệt.

Đó là lúc automation testing có ý nghĩa thật sự, không phải vì nó "hot" hay vì JD nào cũng ghi.

Selenium WebDriver là tool automation web phổ biến nhất hiện tại. Ý tưởng đơn giản: thay vì bạn ngồi click, Selenium điều khiển browser giả lập đúng thao tác người dùng - click, nhập text, scroll, chuyển tab. Dev không biết đó là bot hay người thật.

343496 Selenium điều khiển browser y hệt người dùng thật - không có gì khác biệt với server

Vì sao fresher nên học Selenium ngay sau khi nắm manual? Vì:

  • {{fact:fact_0}} automation tester có mức lương cao hơn manual tester cùng kinh nghiệm
  • Portfolio có script automation thực chiến tăng đáng kể cơ hội qua vòng CV
  • Kỹ năng chuyển được sang Playwright, Cypress sau này - nền tảng giống nhau

Bài này mình hướng dẫn từ cài đặt đến các kỹ thuật thực chiến hay gặp nhất trong web phức tạp. Không cần biết code sâu - biết Python cơ bản là đủ.


Cài đặt môi trường: đừng bỏ qua bước này

Nhiều bạn rush qua phần cài đặt rồi loay hoay mãi vì lỗi version. Mình sẽ liệt kê đúng thứ tự.

Bạn cần:

  • Python 3.8+ (khuyến nghị 3.10 hoặc 3.11)
  • Chrome browser (bản mới nhất)
  • ChromeDriver khớp version Chrome
  • Thư viện seleniumwebdriver-manager

Cài qua terminal:

pip install selenium webdriver-manager

Test nhanh xem môi trường chạy không:

# test_setup.py - kiểm tra môi trường trước khi bắt đầu
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

# webdriver-manager tự tải ChromeDriver đúng version - không cần tải thủ công
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

driver.get("https://www.google.com")
print("Title:", driver.title)  # In ra: Google
driver.quit()

343497 webdriver-manager tự match version ChromeDriver - tiết kiệm 80% thời gian setup

Nếu in ra được "Google" - môi trường OK. Phần lớn lỗi setup đến từ 2 nguyên nhân:

Lỗi chromedriver not found: Bạn chưa cài webdriver-manager hoặc pip install thất bại. Chạy lại pip install --upgrade webdriver-manager.

Lỗi SessionNotCreatedException: Version ChromeDriver không khớp Chrome. Dùng webdriver-manager là cách dứt điểm nhất - nó tự detect.

Mình khuyên bạn không dùng headless mode (ẩn browser) khi mới học. Thấy browser chạy giúp bạn debug nhanh hơn nhiều. Sau khi script ổn định rồi mới chuyển headless để chạy CI/CD.


Locator và thao tác cơ bản: nền tảng của mọi script

Trước khi xử lý popup hay Ajax, bạn cần nắm cách Selenium "tìm" element trên trang. Đây là phần mình thấy nhiều fresher hiểu sai nhất.

Selenium tìm element qua locator - giống như bạn mô tả địa chỉ nhà cho shipper. Có 4 locator dùng nhiều nhất:

Locator Dùng khi nào Ví dụ
By.ID Element có id duy nhất By.ID, "username"
By.CSS_SELECTOR Flexible, ngắn gọn By.CSS_SELECTOR, ".btn-primary"
By.XPATH Element không có id/class By.XPATH, "//button[text()='Đặt hàng']"
By.NAME Input form By.NAME, "password"

Ưu tiên: ID > CSS Selector > XPath. XPath chỉ dùng khi không còn cách nào khác - nó brittle (dễ vỡ khi dev thay đổi HTML).

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get("https://demo.nopcommerce.com/login")

# Tìm và nhập email
email_input = driver.find_element(By.ID, "Email")
email_input.clear()  # Xóa text cũ trước khi nhập
email_input.send_keys("[email protected]")

# Nhập password
driver.find_element(By.ID, "Password").send_keys("test123")

# Click nút Login
driver.find_element(By.CSS_SELECTOR, ".login-button").click()

# Verify đăng nhập thành công - kiểm tra URL thay đổi
assert "account" in driver.current_url, "Đăng nhập thất bại!"
print("✓ Đăng nhập thành công")

343498 Ưu tiên CSS Selector hơn XPath - ít vỡ hơn khi dev refactor HTML

Site demo nopcommerce.com là e-commerce mở để test - mình dùng xuyên suốt bài này vì nó có đầy đủ popup, ajax, multi-tab để luyện.

Lưu ý thực chiến: Sau find_element, gọi .clear() trước .send_keys() với input field. Thiếu bước này, text cũ còn trong field sẽ gây lỗi logic mà khó debug.


Xử lý wait và Ajax: lỗi phổ biến nhất của fresher

Mình từng nhận bug report từ một bạn: "Script chạy được hôm qua, hôm nay lỗi không rõ lý do." Kiểm tra lại - bạn ấy dùng time.sleep(2) cứng ở mọi chỗ. Hôm qua server nhanh, hôm nay chậm hơn 1 giây - script vỡ.

time.sleep() là anti-pattern trong automation. Thay vào đó, Selenium có Explicit Wait - chờ đúng đến khi element sẵn sàng, không chờ thừa.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

wait = WebDriverWait(driver, timeout=10)  # Chờ tối đa 10 giây

# Chờ element xuất hiện (DOM có, visible)
try:
    add_to_cart_btn = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, ".add-to-cart-button"))
    )
    add_to_cart_btn.click()
except TimeoutException:
    print("Lỗi: Nút thêm giỏ hàng không xuất hiện sau 10 giây")

# Chờ text Ajax load xong - ví dụ: số lượng giỏ hàng cập nhật
wait.until(
    EC.text_to_be_present_in_element(
        (By.CSS_SELECTOR, ".cart-qty"),
        "1"  # Chờ đến khi hiện số 1
    )
)
print("✓ Giỏ hàng đã cập nhật")

343499 time.sleep() cố định = script dễ vỡ. Explicit Wait = chờ thông minh theo trạng thái thật

Các expected_conditions hay dùng:

  • presence_of_element_located - Element có trong DOM (chưa cần visible)
  • element_to_be_clickable - Element visible VÀ có thể click
  • text_to_be_present_in_element - Chờ text xuất hiện trong element
  • invisibility_of_element_located - Chờ loading spinner biến mất
  • url_contains - Chờ URL chứa chuỗi nhất định (sau redirect)

Pattern chờ spinner Ajax biến mất:

# Sau khi trigger ajax (submit form, click load more...)
# Chờ loading spinner biến mất trước khi verify kết quả
wait.until(
    EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner"))
)
# Bây giờ mới verify dữ liệu đã load
results = driver.find_elements(By.CSS_SELECTOR, ".product-item")
assert len(results) > 0, "Không có sản phẩm nào được load"

Rule của mình: không có time.sleep() trong production script. Nếu thấy mình muốn viết sleep, dừng lại và nghĩ xem Explicit Wait nào phù hợp hơn.


Xử lý popup: 3 loại khác nhau, 3 cách xử lý khác nhau

Popup trong web có 3 dạng hoàn toàn khác nhau về cơ chế. Fresher hay nhầm lẫn 3 loại này nên script không chạy.

1. Browser alert (JavaScript alert/confirm/prompt)

Đây là popup của browser, không phải của web app. Trông khác - không có CSS custom, chỉ có nút OK/Cancel.

# Click trigger alert
driver.find_element(By.ID, "delete-account-btn").click()

# Chuyển focus sang alert
alert = driver.switch_to.alert
print("Nội dung alert:", alert.text)

# Chấp nhận (OK)
alert.accept()

# Hoặc từ chối (Cancel)
# alert.dismiss()

# Nếu là prompt (có input field)
# alert.send_keys("text nhập vào")
# alert.accept()

2. Modal dialog (popup CSS của web app)

Loại phổ biến nhất trong e-commerce: popup khuyến mãi, confirm xóa sản phẩm, popup đăng nhập nhanh. Đây chỉ là HTML element bình thường - dùng find_element như thường.

# Chờ modal xuất hiện
modal = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal-dialog"))
)

# Đóng modal bằng nút X
modal.find_element(By.CSS_SELECTOR, ".close-button").click()

# Hoặc click nút trong modal
modal.find_element(By.CSS_SELECTOR, ".btn-confirm").click()

# Sau khi đóng modal, chờ nó biến mất
wait.until(
    EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal-dialog"))
)

3. Toast notification (thông báo thoáng qua)

Loại khó nhất: xuất hiện rồi tự biến mất sau 3-5 giây. Phải verify ngay lập tức.

# Trigger action tạo toast (ví dụ: thêm vào giỏ hàng)
driver.find_element(By.CSS_SELECTOR, ".add-to-cart").click()

# Chờ toast xuất hiện - timeout ngắn vì toast nhanh
short_wait = WebDriverWait(driver, timeout=3)
toast = short_wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".toast-message"))
)
assert "thêm vào giỏ" in toast.text.lower(), "Toast message sai nội dung"

# Không cần chờ toast biến mất, tiếp tục test

343500 3 loại popup, 3 cơ chế hoàn toàn khác nhau - nhầm là script vỡ ngay


Xử lý multi-tab và iframe: hai kỹ năng ít người để ý

Multi-tab: thanh toán mở tab mới

Scenario hay gặp: click "Thanh toán qua VNPay" - trang mở tab mới. Selenium vẫn focus ở tab cũ nên mọi thao tác sau đó đều fail.

# Lưu handle của tab hiện tại
original_tab = driver.current_window_handle

# Click link mở tab mới
driver.find_element(By.CSS_SELECTOR, ".payment-vnpay").click()

# Chờ tab mới mở
wait.until(EC.number_of_windows_to_be(2))

# Lấy danh sách tất cả tab đang mở
all_tabs = driver.window_handles

# Chuyển sang tab mới (tab khác tab gốc)
for tab in all_tabs:
    if tab != original_tab:
        driver.switch_to.window(tab)
        break

# Bây giờ thao tác trên tab mới
print("Tab mới URL:", driver.current_url)
wait.until(EC.url_contains("vnpay"))

# Sau khi xong, đóng tab mới và quay về tab gốc
driver.close()
driver.switch_to.window(original_tab)
print("✓ Quay về tab gốc")

343501 Quên switch_to.window() là lỗi phổ biến nhất khi test thanh toán multi-tab

iframe: form nhúng trong trang

iframe là trang web nhúng trong trang web khác. Hay gặp ở: form thanh toán thẻ (Stripe, PayPal), bản đồ Google Maps, chatbox. Selenium không thể find element trong iframe nếu chưa switch vào.

# Tìm iframe trước
iframe = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "iframe#payment-frame"))
)

# Switch vào trong iframe
driver.switch_to.frame(iframe)

# Bây giờ find_element hoạt động bình thường trong iframe
driver.find_element(By.ID, "cardNumber").send_keys("4111111111111111")
driver.find_element(By.ID, "expiryDate").send_keys("12/25")
driver.find_element(By.ID, "cvv").send_keys("123")

# QUAN TRỌNG: Phải switch về sau khi xong
driver.switch_to.default_content()

# Bây giờ mới click nút submit ngoài iframe
driver.find_element(By.CSS_SELECTOR, ".submit-payment").click()

Pattern ghi nhớ: Mỗi lần vào iframe bằng switch_to.frame(), ra ngoài bằng switch_to.default_content(). Quên dòng cuối - mọi find_element tiếp theo đều fail.


Drag & drop và thao tác nâng cao

Drag & drop hay xuất hiện trong: sắp xếp ảnh sản phẩm, kéo item vào giỏ, upload file qua drag. Selenium có ActionChains để mô phỏng chuỗi thao tác phức tạp.

from selenium.webdriver.common.action_chains import ActionChains

actions = ActionChains(driver)

# Drag and drop cơ bản
source = driver.find_element(By.ID, "product-img-1")
target = driver.find_element(By.ID, "upload-zone")

actions.drag_and_drop(source, target).perform()

# Hover để trigger dropdown menu
menu_item = driver.find_element(By.CSS_SELECTOR, ".nav-categories")
actions.move_to_element(menu_item).perform()

# Chờ dropdown xuất hiện sau hover
dropdown = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".dropdown-menu"))
)
dropdown.find_element(By.LINK_TEXT, "Điện thoại").click()

# Double click
actions.double_click(
    driver.find_element(By.CSS_SELECTOR, ".product-card")
).perform()

# Right click (context menu)
actions.context_click(
    driver.find_element(By.CSS_SELECTOR, ".file-item")
).perform()

343502 ActionChains gom nhiều thao tác thành 1 chuỗi - giống đúng hành vi người dùng thật

Khi nào dùng ActionChains:

  • Hover trigger dropdown
  • Drag & drop
  • Double click / right click
  • Click và giữ (hold)
  • Keyboard shortcut (Ctrl+A, Ctrl+C...)

Lưu ý quan trọng: Một số site dùng JavaScript library riêng cho drag & drop (jQuery UI, Sortable.js) - ActionChains.drag_and_drop() có thể không work. Lúc đó dùng JavaScript execution:

# Fallback: drag drop qua JavaScript
script = """
var src = arguments[0], tgt = arguments[1];
var dataTransfer = { data: {} };
// Dispatch events thủ công
src.dispatchEvent(new DragEvent('dragstart', {dataTransfer}));
tgt.dispatchEvent(new DragEvent('drop', {dataTransfer}));
src.dispatchEvent(new DragEvent('dragend', {dataTransfer}));
"""
driver.execute_script(script, source, target)

Template script thực chiến cho e-commerce

Mình tổng hợp tất cả kỹ thuật trên thành 1 script hoàn chỉnh - test luồng mua hàng trên nopcommerce.com. Đây là loại script mà bạn có thể đưa vào portfolio khi xin việc.

# ecommerce_test.py - Script test luồng mua hàng hoàn chỉnh
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
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

BASE_URL = "https://demo.nopcommerce.com"

@pytest.fixture(scope="function")
def driver():
    """Setup/teardown driver cho mỗi test"""
    d = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    d.maximize_window()
    yield d
    d.quit()  # Tự đóng sau mỗi test

def test_add_to_cart_and_verify_toast(driver):
    """Test thêm sản phẩm vào giỏ hàng + verify toast notification"""
    wait = WebDriverWait(driver, 10)
    
    # 1. Mở trang sản phẩm
    driver.get(f"{BASE_URL}/build-your-cheap-computer")
    
    # 2. Click thêm vào giỏ
    driver.find_element(By.ID, "add-to-cart-button-1").click()
    
    # 3. Verify toast notification
    toast = wait.until(
        EC.visibility_of_element_located(
            (By.CSS_SELECTOR, "#bar-notification .content")
        )
    )
    assert "shopping cart" in toast.text.lower()
    print("✓ Toast notification hiển thị đúng")

def test_close_popup_and_continue(driver):
    """Test đóng popup khuyến mãi trước khi mua hàng"""
    wait = WebDriverWait(driver, 10)
    driver.get(BASE_URL)
    
    # Một số trang có popup sau 2 giây - dùng Explicit Wait
    try:
        close_btn = WebDriverWait(driver, 5).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ".newsletter-close"))
        )
        close_btn.click()
        print("✓ Đã đóng popup")
    except:
        print("Không có popup - tiếp tục")
    
    # Tiếp tục test bình thường
    driver.find_element(By.CSS_SELECTOR, ".search-box input").send_keys("laptop")

343503 Script có pytest fixture tự động teardown - chuyên nghiệp hơn nhiều so với try/finally thủ công

Tải template đầy đủ tại đây: Tải template script Selenium E-commerce

Script này dùng pytest thay vì chạy thẳng Python. Lý do: pytest tự quản lý pass/fail, generate report, dễ tích hợp CI/CD sau này. Nếu bạn chưa quen pytest, chạy pip install pytest và thay print bằng assert - hiệu quả ngay.

Về portfolio: bạn không cần script 500 dòng để gây ấn tượng. 3-4 test function cover các scenario khác nhau (happy path + edge case + negative test) + README giải thích cách chạy = đủ để phân biệt với 80% ứng viên.


Lỗi thường gặp và cách debug nhanh

Bốn lỗi dưới đây chiếm phần lớn thời gian debug của fresher. Mình liệt kê để bạn nhận ra ngay thay vì Google 30 phút.

NoSuchElementException - Element không tìm thấy

Khả năng cao nhất: element chưa load (thiếu wait), hoặc đang trong iframe chưa switch vào, hoặc locator sai. Debug: mở DevTools, paste selector vào console: document.querySelector('.your-selector') - nếu trả về null thì selector sai.

ElementNotInteractableException - Element có trong DOM nhưng không click được

Thường gặp khi element bị che bởi element khác (overlay, modal, sticky header). Fix:

# Scroll element vào viewport trước khi click
driver.execute_script("arguments[0].scrollIntoView(true);", element)
element.click()

# Hoặc click qua JavaScript (bypass kiểm tra visibility)
driver.execute_script("arguments[0].click();", element)

StaleElementReferenceException - Element đã cũ, DOM đã thay đổi

Xảy ra khi: find element → trang reload/Ajax cập nhật DOM → click element cũ. Fix: find lại element ngay trước khi dùng.

# Sai: find rồi để đó, trang có thể reload
button = driver.find_element(By.ID, "submit")
time.sleep(3)  # Trang reload trong lúc này
button.click()  # StaleElementReferenceException!

# Đúng: find ngay trước khi dùng
driver.find_element(By.ID, "submit").click()

TimeoutException - Chờ quá lâu không thấy element

Debug theo thứ tự: (1) Mở tay, test bằng mắt xem element có xuất hiện không. (2) Kiểm tra selector có đúng không. (3) Tăng timeout thử. (4) Xem element có trong iframe không.

343504 Debug theo đúng thứ tự - tiết kiệm hơn nhiều so với trial and error

Một trick hữu ích: khi không chắc script đang ở đâu, thêm driver.save_screenshot('debug.png') - xem browser đang thấy gì lúc lỗi. Mình dùng trick này nhiều hơn bất kỳ tool debug nào khác.


Bước tiếp theo cho portfolio của bạn

Script chạy được chưa phải là xong. Để portfolio thuyết phục, bạn cần thêm 2 thứ: test reportPage Object Model (POM).

Test report với pytest-html:

pip install pytest-html
pytest ecommerce_test.py --html=report.html --self-contained-html

Chạy xong, file report.html có đầy đủ: số test pass/fail, thời gian chạy, log lỗi. Upload lên GitHub cùng script - recruiter xem được ngay mà không cần chạy code.

Page Object Model - cấu trúc code chuyên nghiệp:

Thay vì viết selector rải rác khắp file, POM gom selector và action của mỗi page vào 1 class:

# pages/login_page.py
class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
        # Gom tất cả selector vào đây
        self.EMAIL_INPUT = (By.ID, "Email")
        self.PASSWORD_INPUT = (By.ID, "Password")
        self.LOGIN_BTN = (By.CSS_SELECTOR, ".login-button")
    
    def login(self, email, password):
        self.driver.find_element(*self.EMAIL_INPUT).send_keys(email)
        self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
        self.driver.find_element(*self.LOGIN_BTN).click()

# Dùng trong test:
def test_valid_login(driver):
    login_page = LoginPage(driver)
    login_page.login("[email protected]", "ValidPass123")
    assert "account" in driver.current_url

343505 POM tách selector ra khỏi test logic - dev thay CSS class, bạn chỉ sửa 1 chỗ thay vì 10 chỗ

Lộ trình học tiếp sau bài này:

  1. Pytest cơ bản - parametrize, fixture, conftest.py
  2. Page Object Model - cấu trúc project thực tế
  3. CI/CD với GitHub Actions - script chạy tự động mỗi khi push code
  4. Playwright - tool mới hơn, API giống Selenium nhưng nhanh hơn và ít flaky hơn

Bạn không cần học hết ngay. Với fresher xin việc, mục tiêu thực tế là: 1 project GitHub có 10-15 test case chạy được, có README, có báo cáo - đó là cái 80% ứng viên không có.

Comment bên dưới nếu bạn gặp lỗi khi chạy script - mình trả lời từng bạn. Chúc luyện tập vui!