PKD / LITE

Từ số 0 đến bài test Selenium + Cucumber đầu tiên

Mục tiêu của bài này không phải là chép vài dòng code để thấy Chrome bật lên. Mục tiêu là hiểu mỗi lớp đang chịu trách nhiệm gì, đọc được log khi test hỏng, và biết sửa đúng chỗ.

Dự án mẫu kiểm thử luồng đăng nhập của SauceDemo. Công nghệ và tên lớp trong bài bám đúng project hiện tại:

  • Java 17+
  • Maven 3.9+
  • Selenium 4.27.0
  • Cucumber 7.20.1
  • JUnit Jupiter 5.11.4
  • JUnit Platform 1.11.4
  • Maven Surefire 3.5.2
  • Package gốc: com.example.saucedemo

🧭 Mô hình tổng thể

Maven
  |
  +-- src/test/resources/config.properties
  |       base.url, username, password
  |
  +-- src/test/resources/features/login.feature
  |       Given / When / Then
  |
  +-- RunCucumberTest (JUnit Platform)
          |
          v
      Cucumber tìm step trong package com.example.saucedemo
          |
          +-- Hooks: tạo và đóng WebDriver
          |       |
          |       +-- DriverFactory: ChromeDriver / FirefoxDriver
          |              |
          |              +-- Selenium Manager tìm driver phù hợp
          |
          +-- LoginSteps: nối câu Gherkin với Java
                  |
                  +-- LoginPage: locator + thao tác UI + explicit wait
                          |
                          +-- Chrome -> https://www.saucedemo.com/

Luồng dễ nhớ: Feature mô tả hành vi -> Steps điều phối -> Page Object thao tác -> Driver điều khiển trình duyệt.

🔎 Ví dụ tham khảo: CucumberJava

[CucumberJava]
    │
    ├─ Java + Selenium + Maven + Cucumber + JUnit
    ├─ Chrome → Google → nhập từ khóa
    ├─ Report: console + HTML + JSON + XML
    ├─ Tham khảo:
    │  https://github.com/pkduong/CucumberJava
    └─ Khi áp dụng: kiểm tra dependency, secret,
       locator và headless CI

Repo dùng để tham khảo luồng chạy và cách đóng gói; không mặc định mọi lựa chọn đều phù hợp production.

🧰 1) Chuẩn bị từ đầu

JDK 17+

Mở PowerShell và kiểm tra:

java -version

Project đặt:

<maven.compiler.release>17</maven.compiler.release>

Vì vậy JDK 17 là mốc tối thiểu của project. JDK mới hơn có thể chạy nếu dependency và môi trường tương thích.

Maven 3.9+

Kiểm tra:

mvn --version

Maven đọc pom.xml, tải dependency vào local repository, biên dịch test và gọi Surefire để chạy JUnit/Cucumber.

Chrome

Cài Chrome trên máy. DriverFactory tạo ChromeDriver, còn Selenium Manager của Selenium 4.27.0 tự xử lý việc tìm hoặc quản lý driver phù hợp. Vì vậy project không cần tải ChromeDriver thủ công và không cần đặt webdriver.chrome.driver.

Vì sao?

Nếu thiếu JDK hoặc Maven, test chưa thể chạy. Nếu thiếu Chrome, nhánh mặc định chrome trong DriverFactory không thể tạo trình duyệt. Ba điều kiện này phải được kiểm tra trước khi đọc lỗi Cucumber.

🗂️ 2) Maven layout của project

CucumberJava/
|-- pom.xml
|-- README.md
|-- src/
|   `-- test/
|       |-- java/com/example/saucedemo/
|       |   |-- config/Config.java
|       |   |-- driver/DriverFactory.java
|       |   |-- hooks/Hooks.java
|       |   |-- pages/LoginPage.java
|       |   |-- runner/RunCucumberTest.java
|       |   `-- steps/LoginSteps.java
|       `-- resources/
|           |-- config.properties
|           `-- features/login.feature
`-- target/                 (Maven sinh ra, khong sua tay)
  • src/test/java: Java code phục vụ test.
  • src/test/resources: feature và cấu hình được đưa lên test classpath.
  • target: class đã biên dịch, report và file tạm; có thể bị xóa rồi sinh lại.
  • pom.xml: danh sách dependency, Java release và plugin chạy test.

Vì sao?

Tách resources khỏi Java giúp feature và cấu hình dễ đọc, dễ thay đổi. Cucumber có thể tìm features trên classpath, còn Config có thể đọc config.properties bằng ClassLoader.

🧩 3) Đọc pom.xml: dependency nào làm việc gì?

Các phiên bản đang dùng trong pom.xml:

Thành phần Dependency / cấu hình Vai trò
Java maven.compiler.release=17 Biên dịch theo Java 17
Selenium org.seleniumhq.selenium:selenium-java:4.27.0 WebDriver, locator, wait, Chrome/Firefox driver
Cucumber Java io.cucumber:cucumber-java:7.20.1 Annotation @Given, @When, @Then
Cucumber JUnit io.cucumber:cucumber-junit-platform-engine:7.20.1 Chạy Cucumber qua JUnit Platform
JUnit Suite org.junit.platform:junit-platform-suite:1.11.4 @Suite, @IncludeEngines, chọn resource
JUnit API org.junit.jupiter:junit-jupiter-api:5.11.4 Assertion assertTrue trong step
Surefire maven-surefire-plugin:3.5.2 Maven tìm và chạy test; sinh report

Cấu hình report hiện tại trong Surefire:

cucumber.plugin=pretty,html:target/cucumber-reports/cucumber.html,json:target/cucumber-reports/cucumber.json
cucumber.publish.quiet=true

Vì sao?

Cucumber không tự chạy chỉ vì có file .feature. Cucumber cần engine, JUnit Platform cần suite, còn Maven cần Surefire để gọi toàn bộ chuỗi đó bằng mvn test.

🔐 4) Cấu hình URL và tài khoản

[config.properties]
  ├─ base.url
  ├─ username
  └─ password
        │ ClassLoader
        ▼
[Config.get(key)] → [LoginPage / LoginSteps]

File hiện tại là src/test/resources/config.properties:

base.url=https://www.saucedemo.com/
username=standard_user
password=secret_sauce

Config.java tải file từ test classpath:

try (InputStream input = Config.class.getClassLoader()
        .getResourceAsStream("config.properties")) {
    if (input == null) {
        throw new IllegalStateException(
                "config.properties was not found on the test classpath");
    }
    properties.load(input);
}

Mọi nơi dùng cấu hình qua:

Config.get("base.url");
Config.get("username");
Config.get("password");

Nếu key không tồn tại hoặc rỗng, Config.get ném IllegalArgumentException.

Với project thật, mật khẩu không nên commit công khai. Bài này giữ credentials mẫu của SauceDemo để reproducing đúng bài test.

Vì sao?

URL và credential là dữ liệu thay đổi, không phải trách nhiệm của Page Object. Đưa chúng vào properties giúp LoginPage chỉ lo thao tác giao diện, còn LoginSteps lấy dữ liệu từ cấu hình.

🧱 5) POM: Page Object Model là gì?

LoginPage đại diện cho màn hình đăng nhập. Nó giữ:

  • WebDriver để gửi lệnh tới browser.
  • Locator usernameInput, passwordInput, loginButton, productsTitle.
  • WebDriverWait dùng lại cho các thao tác cần chờ.
  • Các method mang ý nghĩa nghiệp vụ: open, logIn, isProductsPageDisplayed.
LoginSteps
    |
    | goi logIn(username, password)
    v
LoginPage
    |
    | dung locator va wait
    v
WebDriver -> Chrome -> SauceDemo

Vì sao?

Nếu locator nằm rải rác trong step, thay đổi HTML sẽ buộc sửa nhiều nơi. POM gom locator và thao tác UI vào một lớp, nên step đọc gần với ngôn ngữ người dùng hơn.

🚗 6) DriverFactory và Selenium Manager

DriverFactory.createDriver() đọc hai system property:

browser  = chrome (mac dinh)
headless = false (mac dinh)

Các lựa chọn hiện có:

  • chrome: tạo ChromeDriver và đặt kích thước cửa sổ 1920x1080.
  • firefox: tạo FirefoxDriver.
  • headless=true: chạy không mở cửa sổ thật.
  • browser khác: ném IllegalArgumentException.

Ví dụ phần tạo Chrome:

private static ChromeOptions chromeOptions(boolean headless) {
    ChromeOptions options = new ChromeOptions();
    if (headless) {
        options.addArguments("--headless=new");
    }
    options.addArguments("--window-size=1920,1080");
    return options;
}

Vì sao?

Factory tạo driver ở một chỗ duy nhất. Selenium Manager được Selenium 4.27.0 gọi phía dưới để tìm driver; test không cần biết đường dẫn executable.

🪝 7) Hooks: tạo driver, chụp ảnh, đóng driver

Hooks chạy quanh mỗi scenario:

@Before
public void setUp() {
    DRIVER.set(DriverFactory.createDriver());
}

@After
public void tearDown(Scenario scenario) {
    WebDriver driver = DRIVER.get();
    if (scenario.isFailed() && driver instanceof TakesScreenshot screenshotDriver) {
        scenario.attach(screenshotDriver.getScreenshotAs(OutputType.BYTES),
                "image/png", "failure");
    }
    if (driver != null) {
        driver.quit();
        DRIVER.remove();
    }
}

ThreadLocal<WebDriver> tách driver theo thread. Hooks.driver() trả driver cho step; nếu gọi quá sớm, nó báo WebDriver has not been initialized.

Vì sao?

Mỗi scenario cần trạng thái browser sạch. quit() giải phóng browser sau test; screenshot khi fail giúp nhìn thấy màn hình thực tế tại thời điểm lỗi.

🥒 8) Feature Cucumber: viết hành vi trước

File src/test/resources/features/login.feature:

Feature: SauceDemo login

  As a SauceDemo user
  I want to log in with valid credentials
  So that I can access the products page

  Scenario: Login succeeds with valid credentials
    Given I am on the SauceDemo login page
    When I log in with valid credentials
    Then I should see the products page

Given, When, Then mô tả hành vi, không mô tả chi tiết CSS hay WebDriver. Đây là lý do feature có thể được đọc bởi người không viết Java.

Vì sao?

Feature là hợp đồng hành vi. Chi tiết “tìm phần tử bằng id nào” thuộc Page Object, không nên làm feature khó đọc.

🔗 9) Steps: nối Gherkin với Java

LoginSteps ánh xạ từng câu trong feature:

@Given("I am on the SauceDemo login page")
public void openLoginPage() {
    loginPage = new LoginPage(Hooks.driver());
    loginPage.open();
}

@When("I log in with valid credentials")
public void logInWithValidCredentials() {
    loginPage.logIn(Config.get("username"), Config.get("password"));
}

@Then("I should see the products page")
public void verifyProductsPage() {
    assertTrue(loginPage.isProductsPageDisplayed(),
            "The products page should be displayed after login");
}

Vì sao?

Step chỉ điều phối: lấy driver, lấy dữ liệu cấu hình, gọi Page Object và assert kết quả. Nó không nên chứa nhiều locator hoặc logic chờ riêng lẻ.

▶️ 10) JUnit runner: điểm bắt đầu của Maven

RunCucumberTest là JUnit Platform suite:

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME,
        value = "com.example.saucedemo")
public class RunCucumberTest {
}
  • @Suite: đây là một suite JUnit.
  • @IncludeEngines("cucumber"): dùng Cucumber engine.
  • @SelectClasspathResource("features"): tìm feature trong src/test/resources/features.
  • GLUE_PROPERTY_NAME: tìm step, hook và các class Cucumber trong com.example.saucedemo.

Vì sao?

Không có runner đúng, Maven có thể biên dịch Java nhưng không tìm thấy scenario để chạy. Runner là điểm nối giữa JUnit Platform và Cucumber.

🏃 11) Chạy project và đọc output

[clean test-compile] → [mvn test]
                              │
                 ┌────────────┴────────────┐
                 ▼                         ▼
          [Chrome / Firefox]       [HTML + JSON report]

Biên dịch cả main/test code:

mvn clean test-compile

Chạy test bình thường, mở Chrome:

mvn test

Chạy headless:

mvn test -Dheadless=true

Chọn Firefox, vì DriverFactory có hỗ trợ:

mvn test -Dbrowser=firefox

Các report được ghi vào:

target/cucumber-reports/cucumber.html
target/cucumber-reports/cucumber.json

Output thành công thường có ba bước màu xanh:

Given I am on the SauceDemo login page
When I log in with valid credentials
Then I should see the products page

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Lưu ý: Maven có thể chạy xong nhưng log vẫn chứa WARNING. Hãy phân biệt WARNING với ERROR, Failures và BUILD FAILURE.

🐞 12) Lỗi thực tế: NoSuchElementException tại #user-name

Lỗi người dùng gặp có dạng bản chất như sau:

org.openqa.selenium.NoSuchElementException:
Unable to locate element: {"method":"css selector","selector":"#user-name"}

Trong log còn thể hiện implicit wait bằng 0. Điều đó có nghĩa là lệnh tìm phần tử chỉ kiểm tra một lần tại thời điểm đó. Nếu trang chưa tải xong hoặc form chưa xuất hiện, Selenium ném lỗi ngay.

driver.findElement(By.id("user-name"))
             |
             +-- kiem tra ngay lap tuc
             +-- implicit wait = 0
             +-- chua co element -> NoSuchElementException

Cách đọc lỗi

  1. Đọc loại exception: NoSuchElementException nói rằng Selenium không tìm thấy element tại thời điểm tìm.
  2. Đọc locator: #user-name tương đương By.id("user-name"); locator của SauceDemo là hợp lệ.
  3. Đọc wait: implicit wait 0 cho thấy không có thời gian chờ ngầm.
  4. Đọc step đang fail: lỗi xảy ra trong thao tác login, trước assertion products page.
  5. Kiểm tra URL và screenshot: có thể trang chưa mở đúng URL, mạng chậm, hoặc browser đang ở trang khác.

Vì sao không nên chỉ tăng implicit wait?

Implicit wait là cấu hình toàn cục cho mọi findElement. Nó có thể làm test chậm và khó đoán khi trộn với explicit wait. Với trạng thái cụ thể như “username đã hiển thị”, explicit wait diễn đạt đúng ý định hơn.

🛠️ 13) Sửa đúng chỗ bằng WebDriverWait

[findElement ngay]
        │ element chưa sẵn sàng
        ▼
[NoSuchElementException]
        │ sửa bằng
        ▼
[WebDriverWait + điều kiện đúng]

Trước: tìm ngay lập tức

Đây là kiểu code gây lỗi khi implicit wait bằng 0:

public void logIn(String username, String password) {
    driver.findElement(usernameInput).sendKeys(username);
    driver.findElement(passwordInput).sendKeys(password);
    driver.findElement(loginButton).click();
}

Nếu thao tác open() cũng chỉ gọi driver.get(...) mà không chờ form, rủi ro còn rõ hơn:

public void open() {
    driver.get(Config.get("base.url"));
}

Vì sao?

driver.get chờ một phần quá trình tải trang, nhưng không nên giả định rằng mọi element động đã sẵn sàng đúng vào thời điểm lệnh tiếp theo chạy. Mạng, máy và browser có tốc độ khác nhau.

Sau: chờ đúng trạng thái cần thao tác

LoginPage hiện tại đã dùng hướng này:

public LoginPage(WebDriver driver) {
    this.driver = driver;
    this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}

public void open() {
    driver.get(Config.get("base.url"));
    wait.until(ExpectedConditions.visibilityOfElementLocated(usernameInput));
}

public void logIn(String username, String password) {
    wait.until(ExpectedConditions.visibilityOfElementLocated(usernameInput))
            .sendKeys(username);
    wait.until(ExpectedConditions.visibilityOfElementLocated(passwordInput))
            .sendKeys(password);
    wait.until(ExpectedConditions.elementToBeClickable(loginButton)).click();
}

Cách đọc từng dòng:

  • Duration.ofSeconds(10): tối đa chờ 10 giây cho mỗi điều kiện.
  • visibilityOfElementLocated(usernameInput): element phải tồn tại và nhìn thấy được.
  • elementToBeClickable(loginButton): nút phải sẵn sàng để click.
  • Nếu hết 10 giây, lỗi sẽ là timeout có ngữ cảnh hơn, thay vì thất bại tức thời.

Explicit wait không chữa được URL sai, selector sai hoặc server không phản hồi. Nó chỉ xử lý đúng bài toán đồng bộ hóa khi element xuất hiện muộn.

Cải thiện tiếp theo cho assertion

isProductsPageDisplayed() hiện kiểm tra URL rồi gọi ngay:

return driver.getCurrentUrl().contains("/inventory.html")
        && driver.findElement(productsTitle).getText().equals("Products");

Đây là điểm có thể làm explicit-wait robust hơn ở bước tiếp theo, chẳng hạn chờ productsTitle visible trước khi đọc text. Bài này không sửa code đó; chỉ ghi nhận để tránh nhầm rằng mọi điểm trong flow đã có wait.

⚠️ 14) Cảnh báo CDP với Chrome 153

Log thực tế còn có:

WARNING: Unable to find CDP implementation matching 153
WARNING: Unable to find version of CDP to use for 153.0.8010.53

Giải thích:

  • Chrome đang là nhánh major 153.
  • Selenium 4.27.0 có các module CDP cụ thể cho một số phiên bản Chromium, nhưng không có implementation khớp 153 trong bộ dependency hiện tại.
  • Đây là WARNING, không phải nguyên nhân trực tiếp của NoSuchElementException.
  • Test login có thể vẫn chạy thành công nếu chỉ dùng WebDriver API thông thường. Log chạy thành công gần nhất của project có Failures: 0, nhưng vẫn in cảnh báo này.

CDP đặc biệt quan trọng khi test dùng DevTools API như network interception, performance log hoặc browser emulation. Với flow hiện tại chỉ dùng ChromeDriver, locator, click và wait, cần theo dõi warning nhưng không nên chữa nhầm nó thành lỗi locator.

Vì sao?

Hai vấn đề khác nhau:

NoSuchElementException + implicit wait 0
    -> vấn đề đồng bộ element

Unable to find CDP implementation matching 153
    -> lệch phiên bản Chrome/CDP integration

Ưu tiên sửa explicit wait cho lỗi element. Nếu sau này cần CDP API, cân nhắc cập nhật Selenium tương thích với Chrome hoặc thêm module CDP đúng theo hướng dẫn của chính Selenium; không tự đoán artifact version.

📋 15) Checklist xử lý lỗi

[ ] java -version >= 17
[ ] mvn --version >= 3.9
[ ] Chrome đã cài và mở được
[ ] base.url có dấu / đúng và truy cập được
[ ] config.properties nằm trong src/test/resources
[ ] username/password không rỗng
[ ] feature nằm trong src/test/resources/features
[ ] runner chọn đúng resource "features"
[ ] glue trỏ tới com.example.saucedemo
[ ] locator #user-name đúng với trang đang mở
[ ] URL thực tế là https://www.saucedemo.com/
[ ] dùng explicit wait cho element xuất hiện/click được
[ ] screenshot failure được xem trong report
[ ] phân biệt WARNING với BUILD FAILURE
[ ] kiểm tra target/cucumber-reports/cucumber.html

Nếu vẫn lỗi #user-name, kiểm tra theo thứ tự:

  1. In hoặc quan sát URL hiện tại; có thể browser bị redirect hoặc không tải được mạng.
  2. Mở base.url bằng tay trên Chrome.
  3. Xem screenshot do Hooks attach khi scenario fail.
  4. Xác nhận LoginPage dùng By.id("user-name"), không gõ nhầm username.
  5. Giữ explicit wait và kiểm tra timeout có đủ 10 giây trong môi trường chậm.
  6. Nếu lỗi là TimeoutException, đó là dấu hiệu element không xuất hiện trong thời gian chờ; quay lại kiểm tra URL, mạng và DOM.

🧠 16) Tổng kết: ai làm gì?

Thành phần Trách nhiệm chính Không nên làm
pom.xml Dependency, Java release, Surefire, report Chứa locator UI
Config Đọc properties và báo thiếu key Điều khiển browser
DriverFactory Tạo Chrome/Firefox và options Chứa step nghiệp vụ
Hooks Setup, teardown, screenshot Chứa locator login
LoginPage Locator, thao tác UI, explicit wait Đọc trực tiếp feature
LoginSteps Nối Gherkin với Page Object, assertion Rải locator khắp step
login.feature Mô tả hành vi người dùng Chứa Java code
RunCucumberTest Chọn Cucumber engine, feature, glue Thực hiện thao tác UI

Công thức cuối: Feature nói cần gì, Step điều phối, Page Object biết làm thế nào, Hook quản lý vòng đời, DriverFactory biết tạo browser, Maven biết cách chạy tất cả.

Khi test fail, đừng sửa theo dòng log cuối cùng một cách máy móc. Hãy hỏi: đây là locator sai, trang sai, browser chưa sẵn sàng, hay chỉ là warning tương thích? Với lỗi hiện tại, câu trả lời là đồng bộ hóa element: dùng WebDriverWait và ExpectedConditions trong LoginPage.

🎯 Kết luận

Một project Selenium + Cucumber dễ bảo trì khi mỗi lớp có trách nhiệm rõ: feature mô tả hành vi, step điều phối, Page Object thao tác UI, hook quản lý vòng đời và Maven chạy test. Khi lỗi xảy ra, cần phân biệt lỗi locator, lỗi đồng bộ, lỗi môi trường và cảnh báo tương thích trước khi sửa.

🧳 Tóm tắt bỏ túi (Take away)

Điểm chính Hành động
Môi trường Kiểm tra JDK, Maven, browser và cấu hình trước khi debug.
Page Object Giữ locator và explicit wait trong LoginPage, không rải logic UI vào step.
Debug Đọc exception, URL, screenshot và report cùng nhau.
Tham khảo Có thể xem project CucumberJava, nhưng phải tự xác minh dependency và yêu cầu CI.