21
0
CHƯƠNG 3: CÁC KỸ THUẬT KIỂM THỬ TĨNH (STATIC TESTING)
1. TỔNG QUAN VỀ KIỂM THỬ TĨNH
Kiểm thử tĩnh là phương pháp kiểm thử phần mềm được thực hiện trực tiếp trên các sản phẩm trung gian (work products) như tài liệu đặc tả, tài liệu thiết kế và mã nguồn mà không cần thực thi chương trình.
Mục tiêu cốt lõi: Phát hiện sớm các khiếm khuyết (defects) ngay từ giai đoạn đầu của vòng đời phát triển phần mềm (SDLC), giúp giảm thiểu tối đa chi phí sửa lỗi (Rework cost).
+ Rà soát (Review): Thực hiện thủ công bởi con người để đánh giá tài liệu và mã nguồn.
+ Phân tích tĩnh (Static Analysis): Sử dụng các công cụ tự động để kiểm tra cấu trúc mã, các quy ước lập trình và lỗ hổng bảo mật.
2. KỸ THUẬT RÀ SOÁT PHẦN MỀM (SOFTWARE REVIEW)
Rà soát là hoạt động mang tính hệ thống nhằm đánh giá chất lượng sản phẩm. Trong đó, Thanh kiểm tra (Inspection) là hình thức rà soát chính thức và chặt chẽ nhất (theo mô hình Fagan Inspection).
2.1. Quy trình Thanh kiểm tra tiêu chuẩn
Bước 1. Lập kế hoạch: Xác định phạm vi, mục tiêu, lựa chọn nhân sự và chuẩn bị tài liệu liên quan.
Bước 2. Giới thiệu: Tác giả trình bày tổng quan về mục tiêu và logic xử lý của sản phẩm cần rà soát.
Bước 3. Chuẩn bị cá nhân: Từng thành viên đọc tài liệu độc lập, sử dụng checklist để phát hiện các lỗi tiềm ẩn trước cuộc họp.
Bước 4. Họp kiểm tra: Nhóm tập trung thảo luận, ghi nhận các lỗi đã tìm thấy. Chỉ tập trung phát hiện lỗi, không giải quyết tại cuộc họp.
Bước 5. Sửa lỗi: Tác giả thực hiện điều chỉnh sản phẩm dựa trên biên bản ghi chép.
Bước 6. Theo dõi: Người điều phối xác nhận các lỗi đã được sửa triệt để và đúng yêu cầu.
2.2. Vai trò và Trách nhiệm
+ Người điều phối (Moderator): Lên kế hoạch, dẫn dắt cuộc họp và đảm bảo quy trình được tuân thủ. Cần là người có kinh nghiệm và khách quan.
+ Tác giả (Author): Người tạo ra sản phẩm. Có trách nhiệm giải trình và tiếp thu các đóng góp để chỉnh sửa.
+ Người rà soát (Reviewer): Những chuyên gia phân tích sản phẩm dưới nhiều góc độ (logic, bảo mật, hiệu suất).
+ Người ghi chép (Recorder/Scribe): Ghi lại tất cả các lỗi và vấn đề nảy sinh trong cuộc họp.
3. HỆ THỐNG DANH MỤC KIỂM TRA (CHECKLIST)
Checklist là công cụ hỗ trợ quan trọng giúp chuẩn hóa quá trình kiểm thử tĩnh, tránh bỏ sót các lỗi phổ biến:
+ Truy xuất & Khai báo dữ liệu: Kiểm tra biến chưa khởi tạo, chỉ số mảng vượt phạm vi, ép kiểu sai hoặc tên biến không tường minh.
+ Tính toán & So sánh: Phát hiện lỗi chia cho 0, tràn số, sai thứ tự ưu tiên phép toán hoặc nhầm lẫn giữa các toán tử logic (== vs =, && vs ||).
+ Luồng điều khiển (Control Flow): Kiểm tra các vòng lặp vô tận, các nhánh điều kiện không bao giờ được thực hiện (Dead code).
+ Giao tiếp & Giao diện: Kiểm tra tính khớp nối giữa tham số thực và tham số hình thức, việc đóng/mở tài nguyên (file, connection).
4. CÁC YẾU TỐ ĐẢM BẢO THÀNH CÔNG (CRITICAL SUCCESS FACTORS)
Để kiểm thử tĩnh đạt hiệu quả chuyên nghiệp, tổ chức cần chú trọng:
+ Văn hóa không đổ lỗi: Rà soát là để cải thiện chất lượng sản phẩm, không phải để đánh giá năng lực cá nhân.
+ Sự tham gia của các bên: Đảm bảo những người tham gia có đủ chuyên môn và kỹ năng phù hợp.
+ Sử dụng Checklist phù hợp: Checklist cần được cập nhật liên tục từ dữ liệu lỗi trong quá khứ.
+ Phân bổ thời gian hợp lý: Kiểm thử tĩnh cần được đưa vào kế hoạch dự án chính thức, không nên thực hiện sơ sài do áp lực tiến độ.
5. KẾT LUẬN
Kiểm thử tĩnh không thay thế cho kiểm thử động nhưng là "phòng tuyến đầu tiên" hiệu quả nhất. Việc kết hợp cả hai phương pháp giúp tối ưu hóa chất lượng phần mềm, đảm bảo hệ thống vận hành ổn định và tiết kiệm tài nguyên cho doanh nghiệp.
6. BÀI TẬP
Xác định lỗi nào trong Checklist và cách sửa:
Đây là lỗi truy xuất dữ liệu trong Checklist.
Vấn đề là biến count được khai báo nhưng chưa được gán giá trị cụ thể, nghĩa là count vẫn là một biến chưa có bất kỳ giá trị nào tồn tại. Khi chương trình truy xuất biến i gán giá trị 10 nhỏ hơn biến count, thì máy lại không đọc được count có giá trị gì để có thể so sánh. Hình dung"
"i = 10 <count = ?"
Gây ra việc chương trình chạy lỗi, không tính toán được.
Cách sửa: Gán giá trị cho count.
int i, count = 10;
for (i = 0; i < count; i++) { ... }
Vấn đề là biến count được khai báo nhưng chưa được gán giá trị cụ thể, nghĩa là count vẫn là một biến chưa có bất kỳ giá trị nào tồn tại. Khi chương trình truy xuất biến i gán giá trị 10 nhỏ hơn biến count, thì máy lại không đọc được count có giá trị gì để có thể so sánh. Hình dung"
"i = 10 <count = ?"
Gây ra việc chương trình chạy lỗi, không tính toán được.
Cách sửa: Gán giá trị cho count.
int i, count = 10;
for (i = 0; i < count; i++) { ... }
Đây là lỗi truy xuất dữ liệu trong Checklist.
Vấn đề là khai báo int list[10]; có nghĩa là mảng có 10 phần tử, bắt đầu từ list[0] kết thúc list[9]. Nhưng chương trình lại truy xuất dữ liệu trong vòng lặp if là list[10], nghĩa là vượt quá giới hạn mảng. Làm lỗi chương trình.
Cách sửa: Gán đúng giá trị giới hạn
if (list[9] == 0) {...}
if (list[8] == 0) {...}
....
if (list[0] == 0) {...}
Vấn đề là khai báo int list[10]; có nghĩa là mảng có 10 phần tử, bắt đầu từ list[0] kết thúc list[9]. Nhưng chương trình lại truy xuất dữ liệu trong vòng lặp if là list[10], nghĩa là vượt quá giới hạn mảng. Làm lỗi chương trình.
Cách sửa: Gán đúng giá trị giới hạn
if (list[9] == 0) {...}
if (list[8] == 0) {...}
....
if (list[0] == 0) {...}
Lời giải:
Đây là lỗi tính toán và truy xuất dữ liệu trong Checklist.
Vấn đề là sai từ đầu ở phần khai báo, trong khi phép chia "/" chỉ áp dụng cho dữ liệu số như int, float, double,.. Thì ở bài lại khai báo s1, s2 là dữ liệu string - Chuỗi ký tự. Điều này không có ý nghĩa.
Phép chia đang được áp dụng sai, nên liệt kê vào lỗi tính toán. Và truy xuất dữ liệu sai.
Cách sửa 1: Khai báo dữ liệu số thay vì dữ liệu ký tự, gán giá trị cụ thể.
double s1 = 10;
double s2 = 5;
double result = s1 / s2;
Cách sửa 2: Chuyển dữ liệu ký tự thành dữ liệu số bằng stod( ) = string to double và gán giá trị cụ thể
string s1 = "10";
string s2 = "5";
double n1 = stod(s1);
double n2 = stod(s2);
double result = n1/ n2;
Vấn đề là sai từ đầu ở phần khai báo, trong khi phép chia "/" chỉ áp dụng cho dữ liệu số như int, float, double,.. Thì ở bài lại khai báo s1, s2 là dữ liệu string - Chuỗi ký tự. Điều này không có ý nghĩa.
Phép chia đang được áp dụng sai, nên liệt kê vào lỗi tính toán. Và truy xuất dữ liệu sai.
Cách sửa 1: Khai báo dữ liệu số thay vì dữ liệu ký tự, gán giá trị cụ thể.
double s1 = 10;
double s2 = 5;
double result = s1 / s2;
Cách sửa 2: Chuyển dữ liệu ký tự thành dữ liệu số bằng stod( ) = string to double và gán giá trị cụ thể
string s1 = "10";
string s2 = "5";
double n1 = stod(s1);
double n2 = stod(s2);
double result = n1/ n2;
Đây là lỗi tính toán và truy xuất dữ liệu trong Checklist.
Biến i chưa được gán giá trị đi kèm. Và khi chạy chương trình như phép tính "i*500" thì máy sẽ không biết i mang giá trị gì. Máy sẽ hiểu theo cách "?*500" và nó sẽ báo lỗi biên dịch.
Còn khai báo biến b với kiểu byte thì chỉ có phạm vi -128 đến 127. Giả sử i = 1, mà lấy 1 nhân 500, thì giá trị sẽ vượt qua kiểu byte. Đó là việc gán một giá trị lớn vào một biến có kích thước nhỏ hơn sẽ gây lỗi chương trình.
Cách sửa 1: Nếu muốn giá trị kết quả lớn, thì thay đổi kiểu dữ liệu phù hợp tránh tràn số. Như đổi kiểu dữ liệu byte sang kiểu dữ liệu int.
int i = 1;
int b;
b = i * 500;
Cách sửa 2: Ép kiểu tường minh, bạn vẫn dùng được kiểu dữ liệu byte.
int i = 1;
byte b;
b = (byte) (i * 500);
Cách sửa 3: Kiểm tra điều kiện trước khi gán
int i = 0;
byte b;
int result = i * 500;
if (result >= - 128 && result <= 127) {
b = (byte) result;} else {
System.out.println("Lỗi: Giá trị vượt quá khả năng lưu trữ của byte");
}
Biến i chưa được gán giá trị đi kèm. Và khi chạy chương trình như phép tính "i*500" thì máy sẽ không biết i mang giá trị gì. Máy sẽ hiểu theo cách "?*500" và nó sẽ báo lỗi biên dịch.
Còn khai báo biến b với kiểu byte thì chỉ có phạm vi -128 đến 127. Giả sử i = 1, mà lấy 1 nhân 500, thì giá trị sẽ vượt qua kiểu byte. Đó là việc gán một giá trị lớn vào một biến có kích thước nhỏ hơn sẽ gây lỗi chương trình.
Cách sửa 1: Nếu muốn giá trị kết quả lớn, thì thay đổi kiểu dữ liệu phù hợp tránh tràn số. Như đổi kiểu dữ liệu byte sang kiểu dữ liệu int.
int i = 1;
int b;
b = i * 500;
Cách sửa 2: Ép kiểu tường minh, bạn vẫn dùng được kiểu dữ liệu byte.
int i = 1;
byte b;
b = (byte) (i * 500);
Cách sửa 3: Kiểm tra điều kiện trước khi gán
int i = 0;
byte b;
int result = i * 500;
if (result >= - 128 && result <= 127) {
b = (byte) result;} else {
System.out.println("Lỗi: Giá trị vượt quá khả năng lưu trữ của byte");
}
Lời giải:
1. Sửa phương thức IsExistsFile (Sửa BUG_01, BUG_03)
BUG_03: Thêm kiểm tra path == null để tránh lỗi NullPointerException.
BUG_01: Nếu path rỗng hoặc trắng, trả về false (vì file không thể tồn tại với đường dẫn rỗng).
public static boolean IsExistsFile(String path) {
// Sửa BUG_03 (null check) & BUG_01 (empty check)
if (path == null || path.trim().equals("")) {
return false;
}
java.io.File file = new java.io.File(path);
return file.exists(); // Rút gọn code thay vì dùng if-else trả về true/false
}
2. Sửa phương thức MainProcess (Sửa BUG_02)
BUG_02: Chuyển lời gọi ReadFile vào bên trong khối if sau khi đã xác nhận file tồn tại.
public static void MainProcess(String path) {
// Kiểm tra tồn tại trước khi làm bất cứ việc gì khác
if (IsExistsFile(path)) {
// Sửa BUG_02: Chỉ đọc file khi chắc chắn file có tồn tại
String content = ReadFile(path);
DisplayFileContent(content);
} else {
System.out.println("File not found.");
}
}
3. Sửa phương thức ReadFile (Sửa BUG_04)
BUG_04: Sử dụng cấu trúc try-with-resources để tự động đóng file sau khi đọc, tránh rò rỉ tài nguyên hệ thống.
public static String ReadFile(String path) {
System.out.println("Reading file: " + path);
StringBuilder content = new StringBuilder();
// Sửa BUG_04: Dùng Try-with-resources để tự động đóng Stream/File
try (java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(path))) {
String line;
while ((line = br.readLine()) != null) {
content.append(line).append("\n");
}
} catch (java.io.IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
return content.toString();
}
4. Sửa phương thức DisplayFileContent (Sửa BUG_05, BUG_06)
BUG_06: Kiểm tra nội dung (data) có null hoặc rỗng không trước khi hiển thị.
BUG_05: Hiển thị nội dung thực tế của file (data) thay vì hiển thị lại đường dẫn (path).
public static void DisplayFileContent(String data) {
// Sửa BUG_06: Kiểm tra dữ liệu hợp lệ trước khi xử lý
if (data == null || data.trim().isEmpty()) {
System.out.println("No content to display.");
return;
}
// Sửa BUG_05: Hiển thị nội dung (data) thay vì path
System.out.println("--- FILE CONTENT ---");
System.out.println(data);
System.out.println("--------------------");
}
Tổng kết mã nguồn sau khi sửa hoàn chỉnh:
code
Java
download
content_copy
expand_less
public class FileHandler {
public static boolean IsExistsFile(String path) {
if (path == null || path.trim().isEmpty()) return false;
return new java.io.File(path).exists();
}
public static void MainProcess(String path) {
if (IsExistsFile(path)) {
String content = ReadFile(path);
DisplayFileContent(content);
} else {
System.out.println("File not found: " + path);
}
}
public static String ReadFile(String path) {
StringBuilder content = new StringBuilder();
try (java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(path))) {
String line;
while ((line = br.readLine()) != null) {
content.append(line).append("\n");
}
} catch (Exception e) {
return null;
}
return content.toString();
}
public static void DisplayFileContent(String data) {
if (data == null || data.isEmpty()) {
System.out.println("Content is empty.");
} else {
System.out.println("Displaying content:\n" + data);
}
}
}
Lời giải:
