Giai đoạn 1: Cú pháp nền tảng
Mục lục (56)
Đối tượng: QA đã đọc/chạy được code Appium nhưng chưa có nền Java hệ thống
Thời lượng: ~25 giờ, 6–7 tuần ở nhịp 4h/tuần
Kết quả mong đợi: đọc bất kỳ file .java trong project automation mà không có dòng nào "hiểu mang mang"
Cách dùng tài liệu này
Ba quy tắc, không thương lượng:
- Gõ lại tất cả code, không copy-paste. Lỗi chính tả là bạn học tốt nhất — nó dạy Linh đọc thông báo lỗi.
- Làm bài tập trước khi xem đáp án. Đáp án nằm cuối mỗi bài. Đoán sai rồi mới xem thì nhớ lâu gấp nhiều lần đọc đáp án đúng ngay.
- Mỗi buổi học phải mở IntelliJ. Đọc tài liệu này mà không chạy code sẽ tạo ra cảm giác hiểu, không tạo ra kỹ năng. Đây là cái bẫy lớn nhất của người tự học.
Tạo một project trống trong IntelliJ tên java-basics, mỗi bài một file .java riêng. Mỗi file có dạng:
public class Bai01 {
public static void main(String[] args) {
// code ở đây
}
}
main là điểm bắt đầu chạy của mọi chương trình Java. Bài 6 sẽ giải thích từng chữ trong dòng đó; giờ cứ coi như khung bắt buộc.
Lịch học gợi ý
| Tuần | Nội dung | Giờ |
|---|---|---|
| 1 | Bài 1 — Biến và kiểu dữ liệu | 4h |
| 2 | Bài 2 — String · Bài 3 — Toán tử & điều kiện | 4h |
| 3 | Bài 4 — Vòng lặp | 4h |
| 4 | Bài 5 — Array · Bài 6 — Method (phần 1) | 4h |
| 5 | Bài 6 — Method (phần 2) | 3h |
| 6 | Bài 7 — Primitive vs Reference | 3h |
| 7 | Bài 8 — Debugger · Bài 9 — Bài tập tổng hợp | 3h |
Bài 1 — Biến và kiểu dữ liệu
1.1 Static typing
Java là ngôn ngữ static typing: mỗi biến phải khai báo kiểu trước khi dùng, và kiểu đó không đổi được suốt đời biến.
int soCase = 177;
soCase = 180; // OK — vẫn là số nguyên
soCase = "180"; // LỖI biên dịch — không thể gán String vào int
Đây là khác biệt lớn nhất so với Python/JavaScript. Cái giá phải trả là gõ nhiều hơn; cái được là compiler bắt lỗi giúp mình trước khi chạy. Trong automation, điều này rất giá trị: sai kiểu dữ liệu bị phát hiện lúc build, không phải lúc test đang chạy trên máy thật.
Cấu trúc khai báo luôn là:
kiểu tênBiến = giáTrị ;
Dấu ; kết thúc mỗi câu lệnh, bắt buộc.
1.2 Bốn kiểu phủ 90% code automation
int soCase = 177; // số nguyên
double tiLePass = 92.5; // số thực
boolean daPass = true; // đúng/sai — chỉ có true hoặc false
String tenBug = "Login sai"; // chuỗi ký tự
Java có 8 kiểu nguyên thủy (primitive): byte, short, int, long, float, double, char, boolean. Thực tế automation dùng int, double, boolean, đôi khi long (timestamp, thời gian chờ) và char. byte, short, float gần như không bao giờ cần.
String viết hoa chữ S, ba kiểu kia viết thường. Lý do: String không phải primitive, nó là một class. Bài 7 sẽ mổ xẻ khác biệt này — nó là nguồn gốc của NullPointerException, lỗi phổ biến nhất trong Java. Giờ chỉ cần nhớ quy tắc viết hoa.
1.3 Quy tắc đặt tên
int soLuongTestCase; // camelCase — đúng chuẩn Java
int so_luong_test_case; // chạy được nhưng không ai viết vậy trong Java
int 2soCase; // LỖI — không bắt đầu bằng số
Biến và method: camelCase. Class: PascalCase (LoginPage, BaseTest). Hằng số: IN_HOA_CO_GACH (DEFAULT_TIMEOUT).
final int TIMEOUT = 30; // final = không cho gán lại
TIMEOUT = 60; // LỖI biên dịch
final dùng rất nhiều trong framework thật, cho những giá trị cấu hình không được phép thay đổi giữa lúc chạy.
1.4 Cái bẫy: phép chia số nguyên
Đây là nội dung quan trọng nhất của bài 1.
int tongCase = 177;
int casePass = 170;
double tiLe = casePass / tongCase * 100;
System.out.println(tiLe);
Kết quả không phải 96.04. Lý do: casePass và tongCase đều là int, nên Java thực hiện phép chia số nguyên — kết quả bị cắt bỏ phần thập phân, không làm tròn:
170 / 177 → 0 (chứ không phải 0.96)
0 * 100 → 0
In ra: 0.0
Việc khai báo double tiLe không cứu được gì, vì phép tính bên phải dấu = đã hoàn tất và ra 0 trước khi được gán vào double. Java tính toán theo kiểu của các toán hạng, không theo kiểu của biến chứa kết quả.
Cách sửa: ép kiểu (casting)
double tiLe = (double) casePass / tongCase * 100;
System.out.println(tiLe); // 96.04519774011299
(double) casePass biến 170 thành 170.0. Khi một toán hạng là double, cả phép tính thành double. Ba cách viết khác cùng tác dụng:
double tiLe = casePass * 100.0 / tongCase; // 100.0 thay vì 100
double tiLe = (double) casePass / tongCase * 100;
double tiLe = 1.0 * casePass / tongCase * 100;
Làm tròn để hiển thị
double tiLe = (double) casePass / tongCase * 100;
System.out.println(String.format(java.util.Locale.US, "%.2f%%", tiLe)); // 96.05%
%.2f = số thực 2 chữ số thập phân. %% = in ra dấu %. Chỉ định Locale.US để dấu thập phân là dấu chấm; nếu bỏ đi, máy cài locale Việt Nam sẽ in 96,05.
Bẫy anh em: chia cho 0
int a = 5 / 0; // Ném ArithmeticException lúc chạy — chương trình dừng
double b = 5.0 / 0; // Infinity — KHÔNG báo lỗi
double c = 0.0 / 0; // NaN (Not a Number) — cũng không báo lỗi
Trong code tính tỉ lệ pass, nếu tổng số case = 0 mà dùng int thì test crash; dùng double thì âm thầm ra Infinity rồi report sai. Luôn kiểm tra trước khi chia.
1.5 Ép kiểu ngược lại — có mất dữ liệu
double x = 96.9;
int y = (int) x;
System.out.println(y); // 96 — CẮT bỏ, không làm tròn
Muốn làm tròn thật:
int y = (int) Math.round(96.9); // 97
1.6 Toán tử tăng giảm
int count = 0;
count++; // count = count + 1 → 1
count--; // → 0
count += 5; // count = count + 5 → 5
count *= 2; // → 10
count++ dùng cực nhiều trong vòng lặp (Bài 4) để đếm số case pass/fail.
Bài tập 1
Làm trong IntelliJ, in kết quả ra console.
1.1 Khai báo 4 biến: tên thiết bị test (String), phiên bản Android (double), số case đã chạy (int), đã pass hết chưa (boolean). In tất cả ra một dòng.
1.2 Đoán kết quả trước khi chạy, ghi ra giấy, rồi chạy để so:
System.out.println(7 / 2);
System.out.println(7.0 / 2);
System.out.println(7 / 2.0);
System.out.println((double) (7 / 2));
System.out.println(1 / 3 * 3);
1.3 Có 354 case-execution, 312 pass, 30 fail, 12 blocked. Tính và in tỉ lệ pass, fail, blocked — mỗi tỉ lệ 2 chữ số thập phân kèm dấu %.
1.4 Tổng thời gian chạy test là 4567 giây. In ra dạng 1 giờ 16 phút 7 giây. Gợi ý: dùng phép chia số nguyên (lần này nó là bạn) và toán tử % (chia lấy dư).
1.5 Tại sao đoạn này in ra 0.30000000000000004?
System.out.println(0.1 + 0.2);
Tự tra Google với từ khóa floating point precision. Đây không phải bug của Java. Câu hỏi phụ: nếu cần tính tiền chính xác (project InkedInn có Stripe), nên dùng kiểu gì thay cho double?
Đáp án bài 1
1.1
String thietBi = "Xiaomi Redmi";
double phienBanAndroid = 13.0;
int soCaseDaChay = 177;
boolean passHet = false;
System.out.println(thietBi + " | Android " + phienBanAndroid
+ " | " + soCaseDaChay + " case | Pass hết: " + passHet);
Dấu + với String là phép nối chuỗi. Khi một bên là String, bên kia tự động được chuyển thành String.
1.2
3 ← chia số nguyên, cắt phần thập phân
3.5 ← có 7.0 nên là phép chia thực
3.5 ← có 2.0, cũng vậy
3.0 ← (7/2) tính TRƯỚC ra 3, rồi mới ép sang double
0 ← 1/3 = 0, rồi 0*3 = 0
Dòng 4 và dòng 5 là trọng tâm: thứ tự thực hiện quyết định kết quả, không phải kiểu của biến chứa.
1.3
int tong = 354, pass = 312, fail = 30, blocked = 12;
System.out.println(String.format(java.util.Locale.US,
"Pass: %.2f%% | Fail: %.2f%% | Blocked: %.2f%%",
pass * 100.0 / tong, fail * 100.0 / tong, blocked * 100.0 / tong));
Ra Pass: 88.14% | Fail: 8.47% | Blocked: 3.39%. Lưu ý có thể khai báo nhiều biến cùng kiểu trên một dòng, cách nhau bằng dấu phẩy.
1.4
int tongGiay = 4567;
int gio = tongGiay / 3600; // 1
int phut = (tongGiay % 3600) / 60; // 16
int giay = tongGiay % 60; // 7
System.out.println(gio + " giờ " + phut + " phút " + giay + " giây");
% lấy phần dư: 4567 % 3600 = 967, 967 / 60 = 16, 4567 % 60 = 7.
1.5 double lưu số theo hệ nhị phân theo chuẩn IEEE 754. Số 0.1 trong hệ 2 là số vô hạn tuần hoàn, giống như 1/3 trong hệ 10 — buộc phải làm tròn khi lưu, nên phép cộng tích lũy sai số. Mọi ngôn ngữ dùng double đều vậy.
Với tiền: dùng BigDecimal (Java), hoặc lưu số nguyên đơn vị nhỏ nhất (cent) như Stripe làm — Stripe API nhận amount: 1050 nghĩa là $10.50, chính vì lý do này. Và hệ quả cho QA: không bao giờ so sánh hai double bằng ==, phải so Math.abs(a - b) < 0.0001.
Bài 2 — String
String là kiểu Linh sẽ dùng nhiều nhất: mọi locator, mọi text lấy từ màn hình, mọi thông báo lỗi đều là String.
2.1 Các method thiết yếu
String s = " Login Failed ";
s.length() // 17 — có method length()
s.trim() // "Login Failed" — bỏ khoảng trắng 2 đầu
s.toLowerCase() // " login failed "
s.toUpperCase()
s.contains("Failed") // true
s.startsWith(" Log") // true
s.endsWith("d ") // true
s.indexOf("Failed") // 8 — vị trí đầu tiên, -1 nếu không tìm thấy
s.replace("Failed", "OK") // " Login OK "
s.isEmpty() // false — độ dài có bằng 0 không
s.isBlank() // false — chỉ chứa khoảng trắng thôi không (Java 11+)
s.charAt(2) // 'L' — ký tự tại vị trí 2
s.substring(2, 7) // "Login" — từ vị trí 2 đến TRƯỚC vị trí 7
Hai điều rất dễ nhầm:
Vị trí đếm từ 0. Ký tự đầu tiên ở vị trí 0. substring(2, 7) lấy các vị trí 2,3,4,5,6 — không lấy vị trí 7. Cận trên luôn loại trừ, quy ước này lặp lại khắp Java.
String là immutable — không thể thay đổi. Mọi method trên đều trả về String mới, không sửa String gốc:
String s = " Login Failed ";
s.trim();
System.out.println(s); // " Login Failed " — KHÔNG đổi gì!
s = s.trim(); // phải gán lại
System.out.println(s); // "Login Failed"
Lỗi quên gán lại này rất phổ biến và im lặng — không báo lỗi, chỉ đơn giản là không có tác dụng.
2.2 Nối chuỗi
String tenBug = "Login";
int soThuTu = 5;
String tieuDe = "Bug #" + soThuTu + ": " + tenBug; // "Bug #5: Login"
Nối trong vòng lặp lớn thì StringBuilder nhanh hơn nhiều, vì mỗi phép + tạo ra một String mới trong bộ nhớ:
StringBuilder sb = new StringBuilder();
sb.append("Case 1: PASS\n");
sb.append("Case 2: FAIL\n");
System.out.println(sb.toString());
\n là ký tự xuống dòng. Các escape khác: \t (tab), \" (dấu ngoặc kép), \\ (dấu backslash).
Dấu \\ rất quan trọng khi viết đường dẫn Windows:
String path = "C:\\Users\\Linh\\project\\config.properties";
2.3 Cắt chuỗi
String csv = "TC001,Login,PASS,iOS";
String[] parts = csv.split(",");
System.out.println(parts[0]); // "TC001"
System.out.println(parts[2]); // "PASS"
split trả về một array (Bài 5). Lưu ý: tham số của split là regular expression, nên vài ký tự phải escape:
"a.b.c".split(".") // SAI — dấu . trong regex nghĩa là "ký tự bất kỳ", ra array rỗng
"a.b.c".split("\\.") // ĐÚNG
"a|b".split("\\|") // dấu | cũng phải escape
2.4 Chuyển đổi kiểu
String s = "177";
int n = Integer.parseInt(s); // String → int
double d = Double.parseDouble("92.5"); // String → double
int x = 177;
String s2 = String.valueOf(x); // int → String
String s3 = "" + x; // cách nhanh, cùng kết quả
Integer.parseInt("abc") ném NumberFormatException lúc chạy. Đọc dữ liệu test từ Excel/JSON rất hay gặp lỗi này khi ô dữ liệu trống hoặc có khoảng trắng — nhớ .trim() trước.
2.5 So sánh String — chỗ này gây bug thật
String a = "PASS";
String b = "PASS";
System.out.println(a == b); // true
Nhìn có vẻ == dùng được. Nhưng:
String c = new String("PASS");
System.out.println(a == c); // false !!
System.out.println(a.equals(c)); // true
Hoặc trường hợp thực tế hơn — text lấy từ màn hình app:
String actual = element.getText(); // "PASS" lấy từ UI
System.out.println(actual == "PASS"); // rất có thể false
System.out.println(actual.equals("PASS")); // true
Quy tắc: luôn dùng .equals() để so sánh String, không bao giờ dùng ==. Lý do đầy đủ ở Bài 7. == so sánh "có phải cùng một ô nhớ không", .equals() so sánh "nội dung có giống nhau không".
Các biến thể hữu ích:
actual.equalsIgnoreCase("pass") // bỏ qua hoa/thường
actual.trim().equals("PASS") // bỏ khoảng trắng trước khi so
Trong automation, getText() rất hay trả về text kèm khoảng trắng hoặc ký tự ẩn. .trim() trước khi so là thói quen tốt.
Bài tập 2
2.1 Cho String log = "2026-07-25 14:32:01 [ERROR] LoginTest failed - element not found";
Tách và in riêng: ngày, giờ, mức độ (ERROR), phần mô tả sau dấu -.
2.2 Viết code kiểm tra một String email có "hợp lệ" không theo 3 điều kiện đơn giản: có chứa @, có chứa . sau @, và không có khoảng trắng. In true/false.
2.3 Cho String id = "PLL_INKAPP_TC_0042"; — lấy ra số 42 dưới dạng int.
2.4 Đoán kết quả trước khi chạy:
String s = "Test";
s.concat(" Case");
s.toUpperCase();
System.out.println(s);
System.out.println("abc".substring(1));
System.out.println("abc".substring(3));
System.out.println("hello".indexOf("z"));
2.5 Tại sao đoạn này crash, và sửa thế nào?
String status = null;
if (status.equals("PASS")) {
System.out.println("OK");
}
Đáp án bài 2
2.1
String log = "2026-07-25 14:32:01 [ERROR] LoginTest failed - element not found";
String[] parts = log.split(" ");
System.out.println("Ngày: " + parts[0]);
System.out.println("Giờ: " + parts[1]);
System.out.println("Mức độ: " + parts[2].replace("[", "").replace("]", ""));
System.out.println("Mô tả: " + log.substring(log.indexOf("-") + 1).trim());
Cách lấy mô tả đáng chú ý: tìm vị trí dấu - rồi cắt từ đó trở đi. Đây là kỹ thuật dùng suốt khi parse log.
2.2
String email = "linh@test.com";
int viTriAt = email.indexOf("@");
boolean hopLe = viTriAt > 0
&& email.indexOf(".", viTriAt) > viTriAt
&& !email.contains(" ");
System.out.println(hopLe);
&& là "và", ! là "không". indexOf(".", viTriAt) tìm dấu . bắt đầu từ vị trí viTriAt. Bài 3 sẽ nói kỹ về các toán tử logic này.
2.3
String id = "PLL_INKAPP_TC_0042";
String[] parts = id.split("_");
int so = Integer.parseInt(parts[parts.length - 1]); // 42
System.out.println(so);
Integer.parseInt("0042") ra 42 — số 0 ở đầu tự mất. parts.length - 1 là vị trí phần tử cuối cùng; xem Bài 5.
2.4
Test ← concat và toUpperCase trả về String MỚI, s không đổi
bc ← substring(1) = từ vị trí 1 đến hết
← substring(3) của chuỗi dài 3 = chuỗi rỗng, KHÔNG lỗi
-1 ← không tìm thấy
Riêng "abc".substring(4) sẽ ném StringIndexOutOfBoundsException. Cận bằng đúng độ dài thì hợp lệ, vượt qua mới lỗi.
2.5 Crash với NullPointerException. status không trỏ tới String nào cả, nên gọi method .equals() trên nó là gọi method của "không có gì".
Sửa — đảo thứ tự so sánh:
if ("PASS".equals(status)) { ... } // an toàn với null, trả về false
Đây là mẹo kinh điển trong Java: đặt giá trị chắc chắn không null lên trước. Hoặc kiểm tra tường minh:
if (status != null && status.equals("PASS")) { ... }
Thứ tự trong && quan trọng: Java kiểm tra từ trái sang, thấy sai là dừng ngay, nên status.equals(...) không bao giờ được chạy khi status là null. Cơ chế này gọi là short-circuit evaluation, Bài 3 sẽ nói rõ.
Bài 3 — Toán tử và điều kiện
3.1 So sánh và logic
== bằng (chỉ dùng cho số và boolean, KHÔNG dùng cho String/object)
!= khác
> < >= <=
&& và — sai một cái là sai
|| hoặc — đúng một cái là đúng
! phủ định
Short-circuit: với &&, nếu bên trái đã false thì bên phải không được chạy. Với ||, nếu bên trái đã true thì bên phải không chạy. Đây không phải chi tiết vụn — nó là cách chuẩn để tránh NullPointerException:
if (element != null && element.isDisplayed()) { ... }
Đảo thứ tự lại thì crash khi element là null.
3.2 if / else if / else
double tiLePass = 88.14;
if (tiLePass >= 95) {
System.out.println("Đạt — có thể release");
} else if (tiLePass >= 80) {
System.out.println("Cần xem lại các case fail");
} else {
System.out.println("Không đạt — chặn release");
}
Java kiểm tra từ trên xuống và dừng ở nhánh đúng đầu tiên. Nên thứ tự các điều kiện rất quan trọng: nếu đảo >= 80 lên trước >= 95 thì nhánh 95 không bao giờ được chạy tới.
Điều kiện trong if bắt buộc phải là boolean. Java không cho dùng số như C hay JavaScript:
int n = 5;
if (n) { } // LỖI biên dịch
if (n != 0) { } // đúng
Với biến boolean, viết trực tiếp — đừng so sánh với true:
boolean daPass = true;
if (daPass) { } // đúng chuẩn
if (daPass == true) { } // chạy được nhưng dư thừa, không ai viết vậy
if (!daPass) { } // nếu chưa pass
3.3 Toán tử ba ngôi
String ketQua = (tiLePass >= 95) ? "PASS" : "FAIL";
Đọc là: nếu điều kiện đúng thì lấy giá trị trước :, sai thì lấy giá trị sau. Gọn cho những phép chọn đơn giản; đừng lồng nhiều tầng vì rất khó đọc.
3.4 switch
Khi cần so một biến với nhiều giá trị cố định:
String platform = "android";
switch (platform) {
case "android":
System.out.println("Dùng AndroidDriver");
break;
case "ios":
System.out.println("Dùng IOSDriver");
break;
default:
System.out.println("Platform không hỗ trợ");
}
break bắt buộc. Thiếu nó, Java chạy tiếp xuống các case dưới — gọi là fall-through, gần như luôn là bug.
Java 14+ có cú pháp mũi tên, an toàn hơn vì không cần break:
switch (platform) {
case "android" -> System.out.println("Dùng AndroidDriver");
case "ios" -> System.out.println("Dùng IOSDriver");
default -> System.out.println("Không hỗ trợ");
}
Bài tập 3
3.1 Viết code phân loại độ ưu tiên bug: số ngày quá hạn > 7 và severity là "Critical" → "Escalate"; severity "Critical" → "High"; quá hạn > 7 → "Medium"; còn lại → "Low".
3.2 Đoán kết quả:
int a = 5;
System.out.println(a > 3 || (10 / 0 > 1));
System.out.println((10 / 0 > 1) || a > 3);
3.3 Dùng switch viết hàm chuyển mã trạng thái Redmine thành tên: 1→New, 2→In Progress, 3→Resolved, 5→Closed, 6→Rejected, khác→Unknown.
3.4 Tại sao đoạn này luôn in "Không tìm thấy" dù text đúng là "Login"?
String text = "Login";
if (text == "Login") {
System.out.println("Tìm thấy");
} else {
System.out.println("Không tìm thấy");
}
Câu hỏi phụ: thực ra đoạn này in gì? Chạy thử. Rồi giải thích tại sao nó lại nguy hiểm hơn là nếu nó sai hẳn.
Đáp án bài 3
3.1
int soNgayQuaHan = 10;
String severity = "Critical";
String uuTien;
if (soNgayQuaHan > 7 && severity.equals("Critical")) {
uuTien = "Escalate";
} else if (severity.equals("Critical")) {
uuTien = "High";
} else if (soNgayQuaHan > 7) {
uuTien = "Medium";
} else {
uuTien = "Low";
}
System.out.println(uuTien);
Điều kiện chặt nhất phải đặt trước — đây là nguyên tắc chung khi viết chuỗi if/else if.
3.2
true ← a > 3 đã true, Java KHÔNG chạy phép 10/0
ArithmeticException ← chạy 10/0 trước, crash ngay
Cùng hai điều kiện, chỉ khác thứ tự, một cái chạy tốt một cái crash. Đó là short-circuit trong thực tế.
3.3
int statusId = 3;
String tenTrangThai = switch (statusId) {
case 1 -> "New";
case 2 -> "In Progress";
case 3 -> "Resolved";
case 5 -> "Closed";
case 6 -> "Rejected";
default -> "Unknown";
};
System.out.println(tenTrangThai);
Cú pháp mũi tên còn dùng được như biểu thức trả về giá trị — gọn hơn hẳn kiểu cũ.
3.4 Câu hỏi này là một cái bẫy có chủ đích. Đoạn code đó thực ra in "Tìm thấy" — nó chạy đúng.
Lý do: cả hai đều là string literal viết trực tiếp trong code, Java lưu chúng vào một vùng nhớ chung gọi là string pool và cho hai biến trỏ vào cùng một ô. Nên == tình cờ đúng.
Và đó chính là điều nguy hiểm: code sai nguyên tắc nhưng vẫn chạy đúng khi test bằng literal. Đến khi thay text bằng element.getText() hoặc dữ liệu đọc từ file — không còn là literal, không nằm trong pool — == lập tức trả về false và test fail không rõ nguyên nhân. Bug loại này rất khó tìm vì nó hoạt động hoàn hảo lúc mình thử tay.
Bài học: không đánh giá code đúng hay sai chỉ vì nó chạy ra kết quả mong đợi một lần.
Bài 4 — Vòng lặp
4.1 for
for (int i = 0; i < 5; i++) {
System.out.println("Case " + i);
}
Ba phần cách nhau bằng ;:
int i = 0— khởi tạo, chạy một lần duy nhấti < 5— điều kiện, kiểm tra trước mỗi lần lặp; còn đúng thì còn lặpi++— chạy sau mỗi lần lặp
In ra Case 0 đến Case 4 — 5 lần, không phải 6. Quy ước từ 0, nhỏ hơn n này khớp với cách đánh vị trí array (Bài 5), nên nó là dạng chuẩn.
4.2 while
Dùng khi không biết trước số lần lặp:
int soLanThu = 0;
boolean thanhCong = false;
while (!thanhCong && soLanThu < 3) {
soLanThu++;
System.out.println("Thử lần " + soLanThu);
// thanhCong = ...
}
Đây chính là mô hình retry trong automation. Luôn phải có điều kiện thoát rõ ràng — thiếu soLanThu < 3 thì vòng lặp chạy vô tận nếu không bao giờ thành công.
do...while chạy thân vòng lặp ít nhất một lần rồi mới kiểm tra điều kiện — ít dùng.
4.3 break và continue
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // bỏ qua lần này, sang lần tiếp
if (i == 6) break; // thoát hẳn vòng lặp
System.out.println(i);
}
In: 0 1 2 4 5
4.4 Vòng lặp lồng nhau
String[] platforms = {"iOS", "Android"};
String[] cases = {"TC001", "TC002"};
for (String p : platforms) {
for (String c : cases) {
System.out.println(p + " - " + c);
}
}
Bốn dòng — đúng bằng 2 platform × 2 case. Đây chính là lý do 177 test case ra 354 case-execution. Cú pháp for (String p : platforms) là enhanced for, đọc là "với mỗi p trong platforms"; nói kỹ ở Bài 5.
Bài tập 4
4.1 In bảng cửu chương 2 đến 9 bằng vòng lặp lồng.
4.2 Đoán kết quả và giải thích:
for (int i = 0; i < 3; i++) {
System.out.print(i + " ");
}
System.out.println();
for (int i = 0; i < 3; ++i) {
System.out.print(i + " ");
}
System.out.println();
int j = 0;
while (j < 3) {
System.out.print(j + " ");
}
4.3 Mô phỏng retry: viết vòng lặp thử tối đa 5 lần, mỗi lần in "Attempt n". Giả lập thành công ở lần thứ 3 bằng if (n == 3) thanhCong = true;. Sau vòng lặp in tổng số lần đã thử.
4.4 Tính tổng các số từ 1 đến 100 chỉ chia hết cho 3 hoặc 5.
4.5 Vòng lặp này chạy bao nhiêu lần?
for (int i = 10; i > 0; i -= 3) {
System.out.println(i);
}
Đáp án bài 4
4.1
for (int i = 2; i <= 9; i++) {
System.out.println("--- Bảng " + i + " ---");
for (int j = 1; j <= 10; j++) {
System.out.println(i + " x " + j + " = " + (i * j));
}
}
Dấu ngoặc quanh (i * j) là bắt buộc. Không có nó, Java nối chuỗi từ trái sang: "2 x 3 = " + 2 rồi + 3 → "2 x 3 = 23". Lỗi này rất hay gặp.
4.2
0 1 2 ← i++
0 1 2 ← ++i, trong vòng for hoàn toàn giống i++
0 0 0 0 0 ... ← VÔ TẬN, vì j không bao giờ tăng
Dòng 3 phải bấm dừng bằng nút stop đỏ trong IntelliJ. Khác biệt i++ và ++i chỉ có ý nghĩa khi dùng giá trị trả về: int a = i++ gán rồi mới tăng, int a = ++i tăng rồi mới gán. Trong phần thứ 3 của for thì không khác gì.
4.3
boolean thanhCong = false;
int n = 0;
while (!thanhCong && n < 5) {
n++;
System.out.println("Attempt " + n);
if (n == 3) thanhCong = true;
}
System.out.println("Đã thử " + n + " lần. Thành công: " + thanhCong);
4.4
int tong = 0;
for (int i = 1; i <= 100; i++) {
if (i % 3 == 0 || i % 5 == 0) {
tong += i;
}
}
System.out.println(tong); // 2418
Dùng || chứ không phải &&. Nếu dùng && sẽ chỉ lấy số chia hết cho cả 3 và 5 (tức bội của 15) — thử cả hai để thấy khác biệt.
4.5 4 lần: in 10, 7, 4, 1. Sau đó i = -2, điều kiện i > 0 sai, dừng.
Bài 5 — Array
5.1 Khai báo
String[] devices = {"Xiaomi Redmi", "iPhone 13", "Pixel 6"};
int[] counts = new int[5]; // 5 phần tử, mặc định đều là 0
counts[0] = 177;
Giá trị mặc định: int/double → 0, boolean → false, String và mọi object → null.
devices.length // 3 — length là THUỘC TÍNH, không có ()
"abc".length() // 3 — String thì là METHOD, có ()
Điểm khác biệt vô lý này là di sản lịch sử của Java. Không có logic để suy ra, chỉ có nhớ.
5.2 Truy cập và cái bẫy vị trí
String[] devices = {"Xiaomi Redmi", "iPhone 13", "Pixel 6"};
devices[0] // "Xiaomi Redmi"
devices[devices.length - 1] // "Pixel 6" — phần tử cuối
devices[3] // ArrayIndexOutOfBoundsException
Array có 3 phần tử thì vị trí hợp lệ là 0, 1, 2. Vị trí cuối luôn là length - 1. Lỗi off-by-one — viết <= thay vì < trong vòng for — là lỗi kinh điển:
for (int i = 0; i <= devices.length; i++) { // SAI, crash ở lần cuối
for (int i = 0; i < devices.length; i++) { // ĐÚNG
Array có kích thước cố định. Đã tạo 3 phần tử thì không thêm được phần tử thứ 4. Cần danh sách thay đổi được thì dùng ArrayList — nội dung của Giai đoạn 3.
5.3 Hai cách lặp
// Enhanced for — gọn, dùng khi không cần biết vị trí
for (String d : devices) {
System.out.println(d);
}
// For thường — khi cần vị trí
for (int i = 0; i < devices.length; i++) {
System.out.println((i + 1) + ". " + devices[i]);
}
Enhanced for đọc là "với mỗi d trong devices". Đây là dạng Linh sẽ gặp liên tục khi xử lý List<WebElement>.
5.4 In array — bẫy nhỏ
System.out.println(devices); // [Ljava.lang.String;@1b6d3586 — vô nghĩa
System.out.println(Arrays.toString(devices)); // [Xiaomi Redmi, iPhone 13, Pixel 6]
Cần import java.util.Arrays; ở đầu file. IntelliJ tự thêm khi bấm Alt+Enter vào chỗ báo đỏ.
Arrays còn vài thứ tiện:
Arrays.sort(counts); // sắp xếp tăng dần, SỬA array gốc
Arrays.asList(devices); // chuyển thành List
Bài tập 5
5.1 Cho int[] thoiGian = {12, 45, 8, 30, 22, 60, 15}; (giây, thời gian chạy mỗi test case). Tính và in: tổng, trung bình, giá trị lớn nhất, nhỏ nhất, và số case chạy trên 20 giây.
5.2 Cho String[] ketQua = {"PASS", "FAIL", "PASS", "PASS", "BLOCKED", "FAIL", "PASS"};
Đếm mỗi loại và in tỉ lệ pass với 2 chữ số thập phân.
5.3 In array theo thứ tự ngược, không dùng thư viện.
5.4 Tìm vị trí của "Pixel 6" trong devices. In -1 nếu không có. Gợi ý: dùng break khi tìm thấy.
5.5 Tại sao đoạn này crash, và crash ở lần lặp thứ mấy?
String[] arr = new String[3];
arr[0] = "a";
arr[1] = "b";
for (String s : arr) {
System.out.println(s.toUpperCase());
}
Đáp án bài 5
5.1
int[] thoiGian = {12, 45, 8, 30, 22, 60, 15};
int tong = 0, max = thoiGian[0], min = thoiGian[0], demTren20 = 0;
for (int t : thoiGian) {
tong += t;
if (t > max) max = t;
if (t < min) min = t;
if (t > 20) demTren20++;
}
System.out.println("Tổng: " + tong + "s");
System.out.println("Trung bình: " + String.format(java.util.Locale.US, "%.2f", (double) tong / thoiGian.length) + "s");
System.out.println("Max: " + max + "s, Min: " + min + "s");
System.out.println("Số case > 20s: " + demTren20);
Chú ý max và min khởi tạo bằng thoiGian[0], không khởi tạo bằng 0. Nếu khởi tạo max = 0 thì với array toàn số âm sẽ ra kết quả sai. Và (double) tong để tránh bẫy chia số nguyên ở Bài 1.
5.2
String[] ketQua = {"PASS", "FAIL", "PASS", "PASS", "BLOCKED", "FAIL", "PASS"};
int pass = 0, fail = 0, blocked = 0;
for (String k : ketQua) {
if (k.equals("PASS")) pass++;
else if (k.equals("FAIL")) fail++;
else blocked++;
}
System.out.println("PASS: " + pass + ", FAIL: " + fail + ", BLOCKED: " + blocked);
System.out.println(String.format(java.util.Locale.US, "Tỉ lệ pass: %.2f%%", pass * 100.0 / ketQua.length));
5.3
for (int i = devices.length - 1; i >= 0; i--) {
System.out.println(devices[i]);
}
Không dùng được enhanced for cho việc này — đó là giới hạn của nó, và cũng là lý do vẫn cần for thường.
5.4
String canTim = "Pixel 6";
int viTri = -1;
for (int i = 0; i < devices.length; i++) {
if (devices[i].equals(canTim)) {
viTri = i;
break;
}
}
System.out.println(viTri);
break khi đã tìm thấy — không cần lặp tiếp. Với array lớn, khác biệt hiệu năng là thật.
5.5 Crash ở lần lặp thứ 3, với NullPointerException. arr[2] chưa được gán nên mang giá trị mặc định null, gọi .toUpperCase() trên null là crash. Hai lần đầu in A và B bình thường.
Đây là lý do phải cẩn thận với new String[n] — nó tạo ra n ô null, không phải n chuỗi rỗng.
Bài 6 — Method
6.1 Cấu trúc
public static double tinhTiLePass(int soPass, int tongCase) {
return (double) soPass / tongCase * 100;
}
Đọc từng phần:
public— phạm vi truy cập, ai cũng gọi đượcstatic— thuộc về class, không cần tạo object; nói ở 6.4double— kiểu trả vềtinhTiLePass— tên, camelCase, nên là động từ(int soPass, int tongCase)— tham số, phải khai báo kiểu từng cáireturn— trả giá trị về và kết thúc method ngay
Gọi:
double tiLe = tinhTiLePass(312, 354);
Method không trả về gì thì kiểu là void:
public static void inBaoCao(String tenSuite, int soCase) {
System.out.println("Suite: " + tenSuite + " (" + soCase + " case)");
}
6.2 Tại sao cần method
Nguyên tắc DRY — Don't Repeat Yourself. Code lặp ba lần là dấu hiệu cần một method. Lợi ích thật: sửa logic một chỗ, mọi nơi gọi đều được sửa. Chính là ý tưởng nền của Page Object Model — LoginPage.login(email, password) viết một lần, dùng ở hàng chục test.
Một method tốt làm một việc, tên nói rõ nó làm gì, và ngắn — quá 20 dòng thì thường là đang làm nhiều việc.
6.3 Nhiều method cùng tên — overloading
public static void login(String email, String password) { ... }
public static void login(String email) { // dùng mật khẩu mặc định
login(email, "123456");
}
Java phân biệt bằng danh sách tham số (số lượng và kiểu), không phải kiểu trả về. Rất phổ biến trong framework: click(element) và click(element, timeout).
6.4 static và không static
Đây là chỗ hầu hết người mới mắc, và nó chuẩn bị cho toàn bộ Giai đoạn 2.
public class TestUtils {
public static String getTimestamp() { ... } // static
public void chayTest() { ... } // không static
}
Gọi:
TestUtils.getTimestamp(); // static: gọi qua tên CLASS
TestUtils utils = new TestUtils(); // không static: phải TẠO OBJECT trước
utils.chayTest();
Cách phân biệt khi nào dùng gì: method có cần dữ liệu riêng của từng object không?
- Không cần — chỉ nhận input, xử lý, trả output →
static. Ví dụ:tinhTiLePass(),getTimestamp(),docFileConfig(). Đây là "hàm tiện ích". - Có cần — phụ thuộc trạng thái của object cụ thể → không static. Ví dụ
login()trongLoginPagecần biếtdrivernào,LoginPagenào.
Quy tắc kéo theo: method static không truy cập được biến/method không static. Lỗi biên dịch non-static method cannot be referenced from a static context mà người mới gặp liên tục chính là từ đây. Lý do đơn giản: method static tồn tại khi chưa có object nào, nên nó không thể biết dữ liệu của object.
Và main là static — vì Java phải chạy nó khi chưa có object nào tồn tại.
6.5 Tham số được truyền như thế nào
public static void tang(int x) {
x = x + 100;
}
public static void main(String[] args) {
int a = 5;
tang(a);
System.out.println(a); // 5 — KHÔNG đổi
}
Java truyền bản sao của giá trị. x là một biến riêng, sửa nó không ảnh hưởng a. Với object thì câu chuyện phức tạp hơn — chính là nội dung Bài 7.
Bài tập 6
6.1 Viết method boolean laHopLe(String email) áp dụng logic từ bài 2.2, rồi gọi thử với 4 email khác nhau.
6.2 Viết method String phanLoaiBug(int soNgayQuaHan, String severity) trả về mức ưu tiên (logic bài 3.1). Gọi và in kết quả cho 4 trường hợp.
6.3 Viết method void inBangKetQua(String[] tenCase, String[] ketQua) in ra dạng bảng đánh số. Xử lý luôn trường hợp hai array khác độ dài — in thông báo lỗi và return sớm.
6.4 Viết ba method overload cùng tên waitAndLog:
waitAndLog(String action)— timeout mặc định 10waitAndLog(String action, int timeout)waitAndLog(String action, int timeout, boolean screenshot)
Method ngắn gọi method dài hơn, đừng lặp code.
6.5 Đoạn này sai ở đâu?
public class Bai06 {
int soCase = 177;
public static void main(String[] args) {
System.out.println(soCase);
}
}
Đáp án bài 6
6.1
public static boolean laHopLe(String email) {
if (email == null) return false;
int viTriAt = email.indexOf("@");
return viTriAt > 0
&& email.indexOf(".", viTriAt) > viTriAt
&& !email.contains(" ");
}
Dòng kiểm tra null đầu tiên rồi return sớm — gọi là guard clause, thói quen rất tốt: xử lý trường hợp bất thường ngay đầu method, phần còn lại không phải lo nữa.
6.2
public static String phanLoaiBug(int soNgayQuaHan, String severity) {
boolean critical = "Critical".equals(severity);
if (soNgayQuaHan > 7 && critical) return "Escalate";
if (critical) return "High";
if (soNgayQuaHan > 7) return "Medium";
return "Low";
}
Vì return kết thúc method ngay, không cần else — dạng này gọn và dễ đọc hơn chuỗi if/else if lồng nhau. Và "Critical".equals(severity) đặt literal trước để an toàn với null.
6.3
public static void inBangKetQua(String[] tenCase, String[] ketQua) {
if (tenCase.length != ketQua.length) {
System.out.println("Lỗi: hai danh sách không cùng độ dài");
return;
}
for (int i = 0; i < tenCase.length; i++) {
System.out.println((i + 1) + ". " + tenCase[i] + " -> " + ketQua[i]);
}
}
return; trong method void nghĩa là "thoát ra ngay", không kèm giá trị.
6.4
public static void waitAndLog(String action) {
waitAndLog(action, 10);
}
public static void waitAndLog(String action, int timeout) {
waitAndLog(action, timeout, false);
}
public static void waitAndLog(String action, int timeout, boolean screenshot) {
System.out.println("[" + action + "] timeout=" + timeout + "s, screenshot=" + screenshot);
}
Chỉ method cuối chứa logic thật, hai method kia chỉ điền giá trị mặc định rồi chuyển tiếp. Đúng mô hình các thư viện automation vẫn dùng.
6.5 soCase là biến của object (không static), còn main là static. Method static không thấy được biến không static → lỗi biên dịch non-static variable soCase cannot be referenced from a static context.
Hai cách sửa:
static int soCase = 177; // cách 1: cho biến thành static
Bai06 b = new Bai06(); // cách 2: tạo object rồi truy cập qua nó
System.out.println(b.soCase);
Cách 2 mới là cách "đúng theo OOP" và là nội dung chính của Giai đoạn 2.
Bài 7 — Primitive vs Reference
Bài này giải thích những gì các bài trước để lại nợ: tại sao == không dùng cho String, NullPointerException từ đâu ra, tại sao String viết hoa. Đọc chậm.
7.1 Hai loại biến
Primitive (int, double, boolean, char...): biến chứa trực tiếp giá trị.
int a = 5;
int b = a; // sao chép giá trị 5
b = 10;
System.out.println(a); // 5 — a không liên quan gì tới b nữa
Reference (String, array, và mọi object): biến chứa địa chỉ tới object nằm ở nơi khác trong bộ nhớ.
int[] x = {1, 2, 3};
int[] y = x; // sao chép ĐỊA CHỈ, không sao chép array
y[0] = 99;
System.out.println(x[0]); // 99 !!! x và y trỏ vào CÙNG một array
Sơ đồ:
PRIMITIVE REFERENCE
a [ 5 ] x [ addr#1 ] ──┐
b [ 10 ] ├──→ #1 [ 99, 2, 3 ]
y [ addr#1 ] ──┘
hai ô nhớ độc lập hai biến, MỘT array
Đây là lý do bài 6.5 nói "Java truyền bản sao của giá trị" mà vẫn đúng: với reference, cái được sao chép là địa chỉ, nên method sửa được nội dung object gốc.
public static void themCase(int[] arr) {
arr[0] = 999; // SỬA được object gốc
}
public static void thayThe(int[] arr) {
arr = new int[]{7, 8, 9}; // KHÔNG ảnh hưởng gì bên ngoài
}
Method thứ nhất sửa nội dung của array mà cả hai bên cùng trỏ tới. Method thứ hai chỉ đổi biến arr cục bộ sang trỏ vào array mới — biến bên ngoài vẫn trỏ vào array cũ.
7.2 == và .equals()
Giờ thì rõ:
==so sánh địa chỉ — "hai biến có trỏ vào cùng một object không?".equals()so sánh nội dung — do class tự định nghĩa
String a = new String("PASS");
String b = new String("PASS");
a == b // false — hai object khác nhau ở hai địa chỉ
a.equals(b) // true — nội dung giống
Với primitive thì không có địa chỉ, nên == là cách so sánh duy nhất và đúng:
int x = 5, y = 5;
x == y // true, luôn đúng
Quy tắc cuối cùng: == cho primitive, .equals() cho mọi thứ còn lại.
Ngoại lệ khó chịu ở giữa — string pool:
String a = "PASS"; // literal → vào string pool
String b = "PASS"; // literal → Java TÁI SỬ DỤNG object trong pool
a == b // true (tình cờ)
Đây là lời giải thích đầy đủ cho bài 3.4: == với literal chạy đúng một cách tình cờ, rồi hỏng ngay khi dữ liệu đến từ getText() hay từ file. Loại bug tệ nhất là loại chạy đúng lúc mình test.
7.3 null và NullPointerException
null nghĩa là "biến này không trỏ tới object nào". Chỉ biến reference mới có thể null — int không bao giờ null được.
String s = null;
s.length(); // NullPointerException
Gọi method của "không có gì" thì crash. NPE là lỗi runtime phổ biến nhất trong Java. Ba nguồn gốc thường gặp trong automation:
String text = element.getText(); // element có thể null nếu chưa tìm thấy
String[] arr = new String[3]; // 3 ô null
String value = map.get("khong_ton_tai"); // Map trả null khi không có key (Giai đoạn 3)
Ba cách phòng:
if (s != null && s.equals("PASS")) { ... } // kiểm tra trước
if ("PASS".equals(s)) { ... } // đảo thứ tự
String an = (s != null) ? s : ""; // giá trị mặc định
Đọc stack trace của NPE: dòng đầu cho biết file và số dòng. Đến đó, tìm xem trên dòng đó có mấy chỗ gọi . — một trong các object đứng trước dấu . đang là null.
Bài tập 7
7.1 Đoán tất cả kết quả trước khi chạy:
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = a;
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(b));
System.out.println(java.util.Arrays.equals(a, b));
7.2 Viết method int[] copyArray(int[] goc) trả về một array mới có cùng nội dung nhưng độc lập hoàn toàn. Chứng minh nó độc lập bằng cách sửa bản copy rồi in bản gốc.
7.3 Đoán kết quả:
public static void doiTen(String s) {
s = "Đã đổi";
}
public static void main(String[] args) {
String ten = "Ban đầu";
doiTen(ten);
System.out.println(ten);
}
Giải thích vì sao — chú ý String là immutable (Bài 2.1).
7.4 Tìm tất cả chỗ có thể ném NPE:
public static String layTrangThai(String[] logs, int index) {
String line = logs[index];
return line.split(" ")[2].toUpperCase();
}
Viết lại cho an toàn.
Đáp án bài 7
7.1
false ← hai array khác nhau, hai địa chỉ khác nhau
true ← c được gán = a, cùng địa chỉ
false ← array KHÔNG override equals(), nên nó so sánh địa chỉ y như ==
true ← Arrays.equals() mới so sánh từng phần tử
Dòng 3 là bẫy tinh vi: .equals() chỉ so sánh nội dung nếu class có định nghĩa nó. String có, array thì không.
7.2
public static int[] copyArray(int[] goc) {
int[] moi = new int[goc.length];
for (int i = 0; i < goc.length; i++) {
moi[i] = goc[i];
}
return moi;
}
// Kiểm tra:
int[] goc = {1, 2, 3};
int[] copy = copyArray(goc);
copy[0] = 999;
System.out.println(goc[0]); // 1 — độc lập, đúng yêu cầu
Có sẵn java.util.Arrays.copyOf(goc, goc.length) làm việc này, nhưng tự viết một lần để hiểu.
7.3 In "Ban đầu". Hai lý do cộng lại: s là biến cục bộ chứa bản sao địa chỉ, và phép gán s = "Đã đổi" chỉ trỏ s sang một String khác — nó không (và không thể) sửa String cũ, vì String là immutable. Biến ten bên ngoài vẫn trỏ vào chuỗi ban đầu.
7.4 Bốn chỗ có thể nổ:
logslà null → NPE ởlogs[index]indexngoài phạm vi →ArrayIndexOutOfBoundsException(không phải NPE nhưng cũng crash)logs[index]là null → NPE ở.split()split(" ")trả về ít hơn 3 phần tử →ArrayIndexOutOfBoundsExceptionở[2]
public static String layTrangThai(String[] logs, int index) {
if (logs == null || index < 0 || index >= logs.length) return "UNKNOWN";
String line = logs[index];
if (line == null) return "UNKNOWN";
String[] parts = line.split(" ");
if (parts.length < 3) return "UNKNOWN";
return parts[2].toUpperCase();
}
Ba if guard clause ở đầu. Code dài hơn nhưng không bao giờ crash — đúng cách viết code framework, nơi một exception làm chết cả suite test.
Bài 8 — Debugger IntelliJ
Kỹ năng có tỉ lệ hoàn vốn cao nhất trong toàn giai đoạn 1. Nó thay System.out.println rải khắp code, và cho phép đọc hiểu code người khác nhanh gấp nhiều lần — trực tiếp hữu ích khi đọc project của NamNT1 hay debug framework Appium.
8.1 Quy trình
- Đặt breakpoint — click vào lề trái, cạnh số dòng. Xuất hiện dấu tròn đỏ.
- Chạy debug —
Shift+F9, hoặc click chuột phải chọnDebug. - Chương trình dừng trước khi thực thi dòng có breakpoint.
- Panel
Variablesdưới cùng hiện giá trị mọi biến đang tồn tại.
8.2 Bốn phím phải thuộc
| Phím | Tên | Tác dụng |
|---|---|---|
F8 |
Step Over | Chạy dòng hiện tại, sang dòng kế. Không vào trong method. |
F7 |
Step Into | Nếu dòng có gọi method, nhảy vào trong method đó. |
Shift+F8 |
Step Out | Chạy hết method hiện tại, quay ra chỗ gọi. |
F9 |
Resume | Chạy tiếp tới breakpoint kế tiếp. |
Cách dùng thực tế: F8 để đi dọc luồng chính, thấy chỗ nghi ngờ thì F7 để vào xem bên trong.
8.3 Hai công cụ nâng cao
Evaluate Expression (Alt+F8): gõ bất kỳ biểu thức Java và xem kết quả ngay tại điểm dừng. Ví dụ đang dừng ở giữa vòng lặp, gõ arr.length - i để xem còn bao nhiêu phần tử. Cực mạnh khi khám phá code lạ.
Conditional breakpoint: click chuột phải vào breakpoint, gõ điều kiện, ví dụ i == 47. Chương trình chỉ dừng khi điều kiện đúng. Không thể thiếu khi lỗi xảy ra ở lần lặp thứ 47 trong 177 lần.
Bài tập 8
8.1 Đặt breakpoint trong bài 5.1, dùng F8 chạy từng bước qua toàn vòng lặp, quan sát tong, max, min đổi ở panel Variables. Sau đó đặt conditional breakpoint t > 40 và xem nó chỉ dừng ở đúng những phần tử nào.
8.2 Chạy debug bài 6.4. Bắt đầu từ waitAndLog("click"), dùng F7 để đi theo chuỗi gọi qua cả ba method. Xem panel Frames (bên trái panel Variables) để thấy chồng lời gọi đang xếp lên nhau.
8.3 Đặt breakpoint ở dòng crash trong bài 5.5. Dùng F9 để dừng lại từng vòng lặp. Ở lần thứ 3, xem panel Variables — s hiện chính xác là gì?
8.4 Chạy debug bài 7.1, tại điểm dừng dùng Alt+F8 gõ a == b, a.equals(b), java.util.Arrays.equals(a, b). So với đáp án đã đọc.
Bài 9 — Bài tập tổng hợp
Không có đáp án cho bài này. Nếu làm được, giai đoạn 1 xong; nếu vướng, quay lại đúng bài liên quan.
9.1 Trình phân tích log
Cho một String[] gồm các dòng log dạng:
2026-07-25 14:32:01 [ERROR] LoginTest failed - element not found
2026-07-25 14:32:15 [INFO] SearchTest passed
2026-07-25 14:33:02 [WARN] StudioTest flaky - retried 2 times
Viết chương trình có các method riêng biệt để:
- Đếm số dòng mỗi mức độ (ERROR/WARN/INFO)
- Lấy danh sách tên test bị ERROR
- Tính khoảng thời gian giữa dòng đầu và dòng cuối (giây)
- In báo cáo tổng hợp
Yêu cầu: mỗi việc một method, main chỉ gọi các method. Không có dòng nào crash khi array rỗng hoặc có phần tử null.
9.2 Bộ tính toán báo cáo test
Cho ba array song song: String[] tenCase, String[] ketQua, int[] thoiGianGiay.
Viết các method tính: tỉ lệ pass, tổng thời gian chạy dạng x giờ y phút z giây, case chạy chậm nhất, danh sách case fail, và một method in bảng có đánh số thẳng cột.
Yêu cầu: xử lý được trường hợp ba array khác độ dài, và trường hợp tất cả case đều blocked (chú ý bẫy chia cho 0 ở Bài 1).
9.3 Đọc code thật
Mở project Appium hiện có. Chọn một file bất kỳ. Với từng dòng, tự trả lời:
- Dòng này khai báo biến, gọi method, hay điều khiển luồng?
- Kiểu của mỗi biến là gì? Primitive hay reference?
- Method này static hay không, và tại sao lại chọn vậy?
- Có chỗ nào có thể ném NPE mà chưa được bảo vệ?
- Có chỗ nào so sánh String bằng
==?
Dòng nào không tự trả lời được — ghi lại. Danh sách đó chính là nội dung Giai đoạn 2 của Linh, và phần lớn sẽ là extends, implements, @Override, this, constructor.
Xong giai đoạn 1 khi nào
Không phải khi đọc hết tài liệu này. Là khi làm được cả ba bài 9 mà không cần mở lại tài liệu, và khi mở một file .java bất kỳ trong project thì không còn dòng nào "hiểu mang mang".
Khi đó, câu hỏi mở đầu — extends BaseTest làm gì, và tại sao gọi được .enterEmail(...).enterPassword(...) nối liền nhau — sẽ là điểm bắt đầu tự nhiên của Giai đoạn 2.