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. WebDriverWaitdù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ạoChromeDrivervà đặt kích thước cửa sổ1920x1080.firefox: tạoFirefoxDriver.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 trongsrc/test/resources/features.GLUE_PROPERTY_NAME: tìm step, hook và các class Cucumber trongcom.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
- Đọc loại exception:
NoSuchElementExceptionnói rằng Selenium không tìm thấy element tại thời điểm tìm. - Đọc locator:
#user-nametương đươngBy.id("user-name"); locator của SauceDemo là hợp lệ. - Đọc wait: implicit wait
0cho thấy không có thời gian chờ ngầm. - Đọc step đang fail: lỗi xảy ra trong thao tác login, trước assertion products page.
- 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
153trong 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ự:
- 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.
- Mở
base.urlbằng tay trên Chrome. - Xem screenshot do
Hooksattach khi scenario fail. - Xác nhận
LoginPagedùngBy.id("user-name"), không gõ nhầmusername. - Giữ explicit wait và kiểm tra timeout có đủ 10 giây trong môi trường chậm.
- 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. |