Khóa luận Sinh ca kiểm thử tham số hóa cho chương trình Java

ĐẠI HỌC QUỐC GIA HÀ NỘI  
TRƯỜNG ĐẠI HỌC CÔNG NGHỆ  
Trần Bình Dương  
SINH CA KIỂM THỬ THAM SỐ HÓA CHO  
CHƯƠNG TRÌNH JAVA  
KHÓA LUẬN TỐT NGHIỆP ĐẠI HỌC HỆ CHÍNH QUY  
Ngành: Công Nghệ Thông Tin  
HÀ NỘI - 2009  
ĐẠI HỌC QUỐC GIA HÀ NỘI  
TRƯỜNG ĐẠI HỌC CÔNG NGHỆ  
Trần Bình Dương  
SINH CA KIỂM THỬ THAM SỐ HÓA CHO  
CHƯƠNG TRÌNH JAVA  
KHÓA LUẬN TỐT NGHIỆP ĐẠI HỌC HỆ CHÍNH QUY  
Ngành: Công Nghệ Thông Tin  
Cán bộ hướng dẫn: TS. Trương Anh Hoàng  
LỜI CẢM ƠN  
Lời đầu tiên em xin được gửi lời cảm ơn chân thành tới TS. Trương Anh Hoàng,  
người thầy đáng kính đã tận tình hướng dẫn em trong suốt thời gian thực hiện khóa  
luận này.  
Em cũng muốn bày tỏ lòng biết ơn đến các thầy cô giáo trường Đại học Công  
Ngh- Đại học Quốc Gia Hà nội, đặc biệt là các thầy cô trong khoa công nghệ thông  
tin đã tận tình dạy dỗ và tạo mọi điều kiện học tập thuận lợi cho em trong suốt bốn  
năm học qua.  
Cuối cùng, em xin gửi lời cảm ơn tới gia đình đình em. Nếu không có tình yêu,  
sự ủng hộ và động viên từ gia đình thì em sẽ không thể hoàn thành khoá luận và có  
được những kết quả như ngày hôm nay.  
Hà nội, 05/2009  
Sinh viên  
Trần Bình Dương  
i
TÓM TẮT NỘI DUNG  
Kiểm thử đơn vị tham số hóa còn đang là một khái niệm mới mẻ đối với nhiều  
nhà phát triển phần mềm. Kiểm thử đơn vị tham số hóa đang dần đóng một vài trò hết  
sức quan trọng trong phát triển phần mềm. Khóa luận này ra đời chính là để nghiên  
cứu về phương pháp kiểm thử mới này và ứng dụng nó cho mục đích kiểm thử các  
chương trình Java. Nội dung khóa luận tập trung vào việc áp dụng khả năng của một  
nền kiểm chứng Java bytecode mã nguồn mở rất hiệu quả và phổ biến hiện nay là Java  
PathFinder để xây dựng một hệ thống hỗ trợ kiểm thử đơn vị tham số hóa cho mục  
đích kiểm thử các chương trình Java. Kết quả của khóa luận là đã xây dựng được một  
hệ thống để thực thi các ca kiểm thử đơn vị tham số hóa viết cho các chương trình Java  
đơn giản. Bên cạnh đó, khóa luận cũng đã trình bày một cách sâu sắc về kiểm thử đơn  
vị tham số hóa và những kỹ thuật phức tạp đằng sau phương pháp kiểm thử mới này  
cũng như một số nghiên cứu liên quan. Qua đó khóa luận kết thúc bằng việc phác thảo  
một số hướng có thể phát triển tiếp để hệ thống này xử lý được các kiểu dữ liệu phức  
tạp hơn.  
ii  
MỤC LỤC  
LỜI CẢM ƠN .....................................................................................................................i  
TÓM TẮT NỘI DUNG.....................................................................................................ii  
MỤC LỤC..........................................................................................................................iii  
CÁC KÝ HIỆU VIẾT TẮT .............................................................................................v  
DANH MỤC HÌNH V...................................................................................................vi  
Chương 1: Kiểm thử đơn vị tham số hóa ......................................................................3  
1.1. Kiểm thử phần mềm.................................................................................................3  
1.2. Kiểm thử đơn vị........................................................................................................4  
1.3. Kiểm thử đơn vị tham số hóa...................................................................................6  
1.3.1. Khái niệm...........................................................................................................6  
1.3.2. Mối quan hệ giữa UT và PUT ..........................................................................7  
1.3.3. Kiểm thử đơn vị tham số hóa với Pex..............................................................8  
1.3.4. Các mẫu kiểm thử tham số hóa ........................................................................9  
1.3.5. Lựa chọn đầu vào kiểm thử với Pex...............................................................10  
Chương 2: Sinh dữ liệu kiểm thử tự động cho PUT ..................................................12  
2.1. Thực thi tượng trưng ..............................................................................................13  
2.1.1. Những khái niệm cơ bản.................................................................................13  
2.1.2. Thực thi tượng trưng tĩnh................................................................................14  
2.1.3. Thực thi tượng trưng động..............................................................................17  
2.2. Xây dựng ràng buc...............................................................................................23  
2.2.1. Lưu trữ giá trị tượng trưng..............................................................................24  
2.2.2. SE với các kiểu dữ liệu nguyên thủy..............................................................25  
2.2.3. SE với đối tượng..............................................................................................28  
2.2.4. SE với các lời gọi phương thức......................................................................30  
2.3. Sinh dữ liệu kiểm thử cho PUT.............................................................................31  
Chương 3: Sinh ca kiểm thử tham số hóa với JPF.....................................................36  
3.1. Kiến trúc của JPF ...................................................................................................36  
3.2. Symbolic JPF..........................................................................................................40  
3.2.1. Bộ tạo chỉ thị....................................................................................................40  
3.2.2. Các thuộc tính..................................................................................................41  
3.2.3. Xử lý các điều kiện rẽ nhánh ..........................................................................42  
3.2.4. Ví d.................................................................................................................43  
3.2.5. Kết hợp thực thi cụ thể và thực thi tượng trưng ............................................47  
iii  
3.3. Sinh PUT với Symbolic JPF..................................................................................48  
3.4. Mở rộng Symbolic JPF ..........................................................................................53  
3.4.1. Các phương pháp cũ........................................................................................53  
3.4.2. Hướng mở rộng ...............................................................................................54  
KẾT LUẬN.......................................................................................................................58  
TÀI LIỆU THAM KHẢO................................................................................................1  
iv  
CÁC KÝ HIỆU VIẾT TẮT  
CFG  
DSE  
JPF  
PC  
Control Flow Graph  
Dynamic Symbolic Execution  
Java PathFinder  
Path Condition  
PUT  
SE  
SET  
TIS  
UT  
Parameterized Unit Test  
Symbolic Execution  
Symbolic Execution Tree  
Test Input Selector  
Unit Test  
v
DANH MỤC HÌNH VẼ  
Hình 1: Mối quan hệ giữa UT và PUT...............................................................................8  
Hình 2 : Cây thực thi tượng trưng ....................................................................................16  
Hình 3: Cây thực thi tượng trưng được quản lý riêng.....................................................22  
Hình 4 : Hệ thống kiểm thử tổng quát..............................................................................24  
Hình 5: Gán giá trị tượng trưng cho tham số đầu vào.....................................................26  
Hình 6: Thực thi tượng trưng với câu lệnh gán...............................................................27  
Hình 7: Thực thi tượng trưng với câu lệnh rẽ nhánh.......................................................28  
Hình 8: Khởi tạo đối tượng làm đầu vào cho chương trình............................................29  
Hình 9. Sinh ra các ràng buộc liên quan tới đối tượng ...................................................30  
Hình 10: Thuật toán sinh dữ liễu kiểm th......................................................................32  
Hình 11: Các cây thực thi cục bộ tương ứng với hàm abs và TestAbs..........................34  
Hình 12: Kiến trúc JPF......................................................................................................37  
Hình 13: Bộ sinh lựa chọn trong JPF...............................................................................39  
Hình 14. Bộ tạo chỉ thị trong JPF.....................................................................................40  
Hình 15. Trạng thái chương trình thực thi trong Symbolic JPF.....................................41  
Hình 16: Bùng nổ việc thực thi tượng trưng trong Symbolic JPF..................................48  
Hình 17: Kiến trúc hệ thống cài đặt .................................................................................49  
Hình 18. Một ví dụ với hệ thống cài đặt ..........................................................................52  
vi  
MỞ ĐẦU  
Trong nền kinh tế hiện nay, ngành công nghiệp phần mềm giữ vai trò hết sức  
quan trọng. Với một số nước có nền công nghệ thông tin phát triển thì ngành công  
nghiệp phần mềm có khả năng chi phối cả nền kinh tế. Tuy nhiên để đảm bảo chất  
lượng cho các phần mềm là một thách thức không nhỏ trong ngành công nghiệp phần  
mềm. Việc phát hiện và khắc phục các lỗi cho các phần mềm là một công việc đòi hỏi  
nhiều nỗ lực và chi phí trong phát triển phần mềm. Với những lĩnh vực ứng dụng ngày  
càng mở rộng của phần mềm hiện nay thì chất lượng phần mềm càng được quan tâm  
hàng đầu. Trong kỹ nghệ phần mềm thì kiểm thử chính là phương pháp dùng để phát  
hiện các lỗi của phần mềm. Trong đó kiểm thử đơn vị là giai đoạn đầu tiên trong quy  
trình kiểm thử. Kiểm thử đơn vị là một công việc bắt buộc trong phát triển phần mềm.  
Theo nghiên cứu của Micorosoft thì có tới 79% các nhà phát triển phần mềm phải viết  
các ca kiểm thử đơn vị để thực hiện việc kiểm thử phần mềm mức đơn vị. Rõ ràng  
kiểm thử đơn vị là một công việc nặng nhọc làm mất nhiều thời gian và chi phí trong  
phát triển phần mềm. Do đó có một phương pháp kiểm thử đơn vị mới đã ra đời giúp  
cải thiện phương pháp kiểm thử đơn vị truyền thống đó là kiểm thử đơn vị tham số  
hóa. Với kiểm thử đơn vị tham số hóa công sức giành cho việc kiểm thử phần mềm  
mức đơn vị đã được giảm đi đáng kể. Kiểm thử đơn vị tham số hóa giúp việc phát hiện  
các lỗi của phần mềm đạt hiệu quả cao hơn do đó nâng cao chất lượng của phần mềm.  
Kiểm thử đơn vị tham số hóa còn là một phương pháp kiểm thử đơn vị còn rất mới và  
nó mới chỉ được áp dụng trong môi trường .NET. Vì vậy việc nghiên cứu về kiểm thử  
đơn vị tham số hóa và ứng dụng nó là một nhu cầu cấp bách. Và khóa luận này ra đời  
chính là vì mục đích này.  
Nội dụng chính của khóa luận gồm 3 chương:  
Chương 1: Trình bày tổng quan về kiểm thử và làm rõ bản chất của kiểm thử đơn  
vị tham số hóa thông qua công cụ Pex của Microsoft.  
Chương 2: Nghiên cứu về phương pháp sinh dữ liệu làm đầu vào kiểm thử cho  
các ca kiểm thử đơn vị tham số hóa. Trong chương này ta cũng sẽ trình bày về một hệ  
thống kiểm thử tổng quát nhất dùng để thực thi các ca kiểm thử đơn vị tham số hóa  
viết cho ngôn ngữ Java.  
1
Chương 3: Trong chương này ta sẽ nghiên cứu về một nền (framework) kiểm  
chứng Java bytecode mã nguồn mở rất phổ biến hiện nay đó là Java PathFinder và áp  
dụng khả năng của nó để xây dựng một nền thực thi các ca kiểm thử tham số hóa viết  
cho những chương trình Java đơn giản. Đồng thời ta cũng đề xuất giải pháp để có thể  
mở rộng Java PathFinder cho mục đích hoàn thiện nền kiểm thmà ta đã xây dựng.  
2
Chương 1: Kiểm thử đơn vị tham số hóa  
1.1. Kiểm thử phần mềm  
Để đảm bảo một hệ thống phần mềm hoặc các thành phần của phần mềm làm  
việc như mong muốn là một thách thức lớn trong ngành công nghiệp phần mềm. Các  
phần mềm lỗi gây ra những tổn thất về kinh tế cũng như những hậu quả nghiêm trọng  
khác tùy thuộc vào lĩnh vực mà phần mềm được sử dụng. Do đó cần phải phát hiện và  
khắc phục các lỗi của phần mềm trước khi đem chúng vào sử dụng. Có các phương  
pháp khác nhau để phát hiện lỗi của phần mềm bao gồm kiểm tra mô hình (model  
checking)[4], các kỹ thuật phân tích tĩnh (static analysis)[24] và kiểm thử (software  
testing)[1].  
Các kỹ thuật phân tích chương trình tĩnh thường đưa ra nhiều cảnh báo  
(warnings) không tương ứng với các lỗi thực sự. Các kỹ thuật kiểm thử phát hiện ra  
các lỗi thực sự nhưng thường không phát hiện ra được tất cả các lỗi do chỉ phân tích  
một số sự thực thi trong chương trình. Trong kiểm tra mô hình, một mô hình của hệ  
thống được tạo ra để hỗ trợ phân tích mọi sự thực thi có thể trong mô hình. Kiểm tra  
mô hình có thể kiểm chứng được rằng mô hình của hệ thống hoạt động chính xác trong  
tất cả trường hợp có thể. Kiểm tra mô hình phân tích hết mọi khía cạnh thực thi của  
chương trình và chỉ ra những sự vi phạm nhưng không chứng minh được chương trình  
sẽ được thực thi chính xác mà không có lỗi. Hạn chế của kiểm tra mô hình đó là không  
gian trạng thái của mô hình thường quá lớn do đó việc thám hiểm tất cả các trạng thái  
không phải lúc nào cũng thực hiện được.  
Kiểm thử chính là kỹ thuật được sử dụng phổ biến nhất để phát hiện và khắc  
phục các lỗi của phần mềm nhằm đảm bảo chất lượng của phần mềm. Chi phí giành  
cho việc kiểm thử chiếm khoảng 50% tổng chi phí trong phát triển phần mềm. Kiểm  
thử là một tiến trình quan trọng trong kỹ nghệ phần mềm. Kiểm thử đơn vị chính là  
bước đầu tiên trong quy trình kiểm thử đó. Có các kỹ thuật kiểm thử khác nhau được  
sử dụng như kiểm thử hộp trắng (white-box testing), kiểm thử hộp đen (black-box  
testing), kiểm thử hộp xám (gray-box testing). Các kỹ thuật kiểm thử đó được dựa trên  
2 loại kiểm thử đó là kiểm thử chức năng (funcional testing) và kiểm thử cấu trúc  
(structured testing). Kiểm thử chức năng là loại kiểm thử dựa trên đặc tả chức năng  
của hệ thống, nó phát hiện các sai sót về chức năng mà không quan tâm tới cài đặt.  
3
Kiểm thử cấu trúc là loại kiểm thử có nghiên cứu mã nguồn bằng việc phân tích thứ tự  
thực hiện các lệnh.  
1.2. Kiểm thử đơn vị  
Kiểm thử đơn vị là một cộng việc quan trọng trong kỹ nghệ phần mềm. Kiểm thử  
đơn vị thường được áp dụng để kiểm tra việc cài đặt của các lớp hoặc phương thức. Để  
thực hiện việc kiểm thử đơn vị, các lớp kiểm thử được tạo ra. Các lớp kiểm thử này  
gồm các phương thức kiểm th. Các phương thức kiểm thử là các phương thức không  
tham số có kiểu trả về là void chứa trong các lớp kiểm thử để kiểm tra các khía cạnh  
cài đặt khác nhau của chương trình. Mỗi phương thức kiểm thử trong các lớp kiểm thử  
biểu thị cho một ca kiểm thử đơn vị (UT).  
Có thể chia một phương thức kiểm thử ra làm 3 phần: Các giá trị đầu vào, dãy  
các lời gọi phương thức, và sự xác nhận (assertions). Kiểm thử thất bại nếu bất cứ sự  
xác nhận nào bị vị phạm hoặc có một ngoại lệ (exception) xảy ra.  
Ví d1.1: Ta xét một phương thức kiểm thử được viết trong nền kiểm thử  
VSUnit.  
public void TestArrayList() {  
// exemplary data  
int capacity = 1;  
object element = null;  
// method sequence  
ArrayList list = new ArrayList(capacity);  
list.Add(element);  
// assertions  
Assert.IsTrue(list[0] == element);  
}
Phương thức kiểm thử TestArrayList bắt đầu bằng việc gán giá trị 1 cho biến  
capacity và giá trị null cho biến element như là các giá trị đầu vào kiểm thử. Sau đó nó  
thực hiện một dãy các lời gọi phương thức, trước tiên là khởi tạo một đối tượng  
ArrayList với kích cỡ là capacity không chứa phần tử nào. ArrayList là mt mảng  
động với kích cỡ có thể thay đổi. Tiếp theo nó chèn một đối tượng là element vào  
mảng. Và cuối cùng là xác nhận xem phần tử đầu tiên của mảng có bằng đối tượng  
vừa được chèn vào hay không.  
Việc cài đặt nhiều phương thức kiểm thử không đảm bảo rằng sẽ kiểm tra được  
hết mọi khía cạnh thực thi của chương trình. Với các chương trình có nhiều đường đi  
thực thi khác nhau thì việc thiếu xót các UT để kiểm tra một vài đường đi thực thi  
trong chương trình là điều thường xuyên xảy ra. Khi người lập trình thay đổi mã cài  
đặt của chương trình được kiểm thử thì nếu như các phương thức kiểm thử không được  
4
cập nhật theo thì sẽ dẫn đến việc nhiều đường đi thực thi của chương trình sẽ không  
được kiểm thử.  
Các nền kiểm thử hỗ trợ viết các UT theo các cách khác nhau. Tuy nhiên, đa  
phần các nền kiểm thử đều cung cấp những dịch vụ (service) như sau:  
+ Cung cấp thuộc tính để chỉ định các phương thức như là các UT.  
+ Phát hiện và thực thi tự động các UT  
+ Một runner với khả năng báo cáo (reporting). Một runner có thể là ứng dụng  
dòng lệnh (console) hoặc là giao diện tích hợp.  
Như trong nền kiểm VSUnit[29] cho môi trường .NET. Ta sử dụng thuộc tính  
[TestClass] để chỉ định một lớp là lớp kiểm thử, và [TestMethod] để chỉ định một  
phương thức như là một phương thức kiểm th. Ngoài ra còn có các thuộc tính khác  
như [ExpectedException<typeofException>] để chỉ định phương thức kiểm thử ném ra  
ngoại lệ của một kiểu ngoại lệ cụ thể nào đó.  
Ví d1.2: Giả sử có một lớp LuhnAlgorithm được cài đặt như sau:  
public static class LuhnAlgorithm {  
public static bool Validate(string number){  
if (number == null)  
throw new ArgumentNullException("");  
foreach (var c in number)  
if (!Char.IsDigit(c))  
throw new ArgumentException("");  
return false;  
}
}
Ta có thể viết một lớp kiểm thử chứa các UT để thực hiện việc kiểm thử lớp  
LuhnAlgorithm:  
[TestClass]// lớp chứa các unit test  
public class LuhnAlgorithmTest {  
[TestMethod]  
[ExpectedException(typeof(ArgumentNullException))]  
public void Test1() {  
LuhnAlgorithm.Validate(null);  
}
[TestMethod]  
[ExpectedException(typeof(ArgumentException))]  
public void Test2() {  
LuhnAlgorithm.Validate("K50");  
}
5
[TestMethod]  
public void Test3() {  
LuhnAlgorithm.Validate(“123”);  
}
}
Khi thực thi phương thức kiểm thử Test1 thì ngoại lệ ArgumentNullException  
được ném ra. Khi thực thi phương thức kiểm thử Test2 thì ArgumentException được  
ném ra. Rõ ràng là mỗi phương thức kiểm thử ở trên chỉ có thể kiểm tra việc thực thi  
của lớp LuhnAlgorithm theo một nhánh đi cụ thể. Thực thi cả 3 phương thức kiểm thử  
ở trên ta sẽ kiểm tra được tất cả các trường hợp thực thi của lớp LuhnAlgorithm. Với  
một chương trình có nhiều đường đi thì ta cần viết các UT khác nhau để kiểm tra sự  
thực thi của chương trình theo các đường đi đó. Tuy nhiên, với những chương trình có  
nhiều đường đi thực thi khác nhau thì việc viết các UT như thế đòi hỏi nhiều thời gian  
và công sức để tính các giá trị đầu vào thích hợp và khó có thể kiểm tra hết được sự  
thực thi của chương trình theo tất cả các đường đi.  
1.3. Kiểm thử đơn vị tham số hóa  
Có rất nhiều các nền kiểm thử khác nhau như JUnit[33] cho Java, NUnit[34],  
VSUnit[29] cho .NET để thực thi các ca kiểm thử đơn vị. Tuy nhiên các nền kiểm thử  
này không hỗ trợ việc sinh tự động các ca kiểm thử đơn vị. Việc viết các ca kiểm thử  
đơn vị để thực thi tất cả các đường đi của một chương trình là một công việc nặng  
nhọc. Giải pháp để giảm công sức cho việc này đó là sử dụng ca kiểm thử đơn vị tham  
số hóa.  
Kiểm thử đơn vị tham số hóa[7, 11, 12] là phương pháp mới trong kiểm thử phần  
mềm. Kiểm thử đơn vị tham số hóa giúp cải thiện nỗ lực trong việc phát triển phần  
mềm. Về bản chất nó chính là sự mở rộng của phương pháp kiểm thử đơn vị truyền  
thống.  
1.3.1. Khái niệm  
Các UT truyền thống là các phương thức kiểm thử không tham số. Ta có thể mở  
rộng các UT đó bằng cách cho phép truyền vào tham số cho các phương thức kiểm  
th. Các ca kiểm thử tham số hóa (PUT) là sự mở rộng của các UT truyền thống. Các  
PUT là các phương thức kiểm thử cho phép nhận các giá trị đầu vào kiểm thử khác  
nhau thông qua tham số đầu vào.  
6
PUT được hiểu ở 2 khía cạnh:  
+ PUT là sự đặc tả về hành vi bên ngoài của chương trình. Nếu như mỗi UT kiểm  
tra sự thực thi của chương trình với những giá trị đầu vào được chọn trước và xác nhận  
kết quả của lần thực thi đó có như mong đợi. Nói cách khác, mỗi UT biểu thị cho một  
hành vi thực thi cụ thể của chương trình. UT chỉ cung cấp các giá trị đầu vào cụ thể  
cho chương trình và xác nhận kết quả thực thi của chương trình với những đầu vào cụ  
thể đó mà không quan tâm tới quá trình thực thi của chương trình với những giá trị đó  
diễn ra như thế nào. Vì thế, UT biểu thị cho hành vi bên ngoài của chương trình. PUT  
cũng xác nhận về hành vị thực thi của chương trình nhưng được mở rộng cho tất cả  
các giá trị đầu vào có thể. Sự xác nhận (assertion) của PUT biểu thị cho các hành vi  
bên ngoài của chương trình. Ở khía cạnh này, PUT giống như sự đặc tả hộp đen  
(black-box) cho lớp được PUT kiểm thử.  
+ Sự lựa chọn các đầu vào kiểm thử cho PUT dẫn đến việc phân tích chương  
trình được kiểm thử. Việc phân tích này cần tìm ra các giá trsao cho khi chương trình  
được thực thi với các giá trị đó thì có nhiều dòng lệnh được chạy (code coverage)[5].  
Nói cách khác, các giá trị cần được lựa chọn sao cho PUT có thể kiểm tra được các  
đường đi thực thi khác nhau của chương trình. Và mỗi UT có thể được sinh ra từ PUT  
bằng cách gọi PUT với mỗi đầu vào cụ thể được chọn. Ở khía cạnh này, PUT là kiểm  
thử hộp trắng (white-box).  
Từ đặc tả về PUT, ta có thể phát biểu vấn đề kiểm thử như sau:  
Cho một chương trình P gồm các câu lệnh S, cần tính toán một tập hợp các đầu  
vào (inputs) I sao cho với tất cả các câu lệnh cần thực thi s trong S, tồn tại đầu vào i  
trong I để P(i) thực thi s.  
1.3.2. Mối quan hệ giữa UT và PUT  
Với mỗi chương trình có mã nguồn ta có thể viết các PUT để mô tả các hành vi  
thực thi của nó. Tuy nhiên, nếu đã có các UT được viết thì ta có thể tái cấu trúc mã cài  
đặt các UT đó để biến chúng thành các PUT. Ngược lại, từ các PUT ta có thể sinh lại  
các UT bằng cách truyền các giá trị cụ thể làm đầu vào cho PUT. Các nền kiểm thử  
đơn vị khác nhau hỗ trợ viết các UT theo các cách khác nhau. Do đó các PUT cũng  
cần được viết sao cho có thể sinh ra các UT tương ứng với các nền kiểm thử đó.  
7
Hình 1: Mối quan hệ giữa UT và PUT  
1.3.3. Kiểm thử đơn vtham số hóa với Pex  
Pex[30] là công cụ mạnh mẽ hỗ trợ việc viết và thực thi các ca kiểm thử tham số  
hóa cho môi trường .NET.  
Ví dụ 1.3: Ca kiểm thử tham số hóa sử dụng Pex :  
Cũng như với UT, ta có thể viết các lớp kiểm thử chứa các ca kiểm thử tham số  
hóa. Với sự hỗ trợ của Pex ta có thể thực thi các ca kiểm thử tham số hóa đó. Tuy  
nhiên không giống việc thực thi các lớp kiểm thử chứa các UT, Pex chỉ thực thi được  
một ca kiểm thử tham số hóa trong mỗi lần chạy.  
[PexMethod]  
// exemplary data  
public void PutArrayList(ArrayList list, object element) {  
// assumptions  
PexAssume.IsTrue(list != null);  
// method sequence  
int len = list.Count;  
list.Add(element);  
// assertions  
PexAssert.IsTrue(list[len] == element);  
}
Ca kiểm thử tham số hóa ở trên là sự mở rộng của UT trong ví dụ 1.1. Rõ ràng  
các giá trị đầu vào của UT trong ví d1.1 là các giá trị cụ thể đã được chuyển thành  
các tham số đầu vào của phương thức. Nếu như UT trong ví d1.1 kiểm thử phương  
thức Add của lớp ArrayList ở khía cạnh là khi chèn một đối tượng có giá trị null vào  
một ArrayList rỗng thì đối tượng được chèn vào trở thành phần tử đầu tiên của  
ArrayList. Thì với ca kiểm thử tham số hóa ở trên phương thức Add được kiểm thử  
với những đối tượng và ArrayList khác. Các ArrayList làm đầu vào kiểm thử có thể là  
ArrayList không chứa phần tử nào hoặc ArrayList đã chứa một số phần tử, hoặc  
ArrayList đã chứa đủ số phần tử so với kích cỡ được cấp phát thì khi chèn một đối  
tượng object có giá trị là null hay các giá trị khác thì đối tượng được chèn vào trở  
thành phần tử cuối cùng của ArrayList.  
8
PUT có thể sử dụng các giả thuyết (assumptions) để giảm kích cỡ miền giá trị  
của tham số đầu vào. Các tham số đầu vào có giá trị thỏa mãn giả thuyết này mới được  
xem xét làm đầu vào kiểm thử. Như với PUT ở trên thì chỉ các ArrayList khác null  
mới được lựa chọn làm đầu vào kiểm thử.  
Khi Pex thực thi ca kiểm thử tham số hóa ở trên sẽ sinh ra Test Suite gồm 2  
VSUnit UT. Đồng thời Pex cũng báo cáo về kết quả xác nhận của ca kiểm thử tham số  
hóa ở trên.  
[TestMethod]  
public void TestAddNoOverflow() {  
PutArrayList(new ArrayList(1), new object());  
}
[TestMethod]  
public void TestAddWithOverflow() {  
PutArrayList(new ArrayList(0), new object());  
}
Với ca kiểm thử tham số hóa này phương thức Add của lớp ArrayList được kiểm  
thử với tất cả các trường hợp thực thi có thể xảy ra.  
Thuộc tính [PexClass] của Pex để xác định một lớp kiểm thử là lớp chứa các ca  
kiểm thử tham số hóa. Thuộc tính [PexMethod] để chỉ định một phương thức kiểm thử  
là ca kiểm thử tham số hóa. Ta muốn viết một lớp kiểm thử chứa các ca kiểm thử tham  
số hóa cần sử dụng thuộc tính [PexClass] và [PexMethod] để viết các ca kiểm thử  
tham số hóa.  
1.3.4. Các mẫu kiểm thử tham số hóa  
Viết các ca kiểm thử tham số hóa là một nghệ thuật. Để viết các ca kiểm thử tham số  
hóa hiệu quả, ta cần thực sự hiểu về mã cài đặt của chương trình mà ta muốn kiểm thử.  
Pex hỗ trợ nhiều mẫu kiểm thử tham số hóa khác nhau[15]. Các mẫu được sử dụng  
nhiều nhất đó là mẫu AAA (Triple-A) và AAAA:  
+ Với mẫu AAA (Arrange, Act, Assert) PUT được tổ chức thành 3 phần:  
Arrange: khởi tạo giá trị các biến sẽ sử dụng  
Act: dãy các lời gọi phương thức  
Assert: sự xác nhận  
+ Với mẫu AAAA, một giả thuyết (Assume) được thêm vào để giới hạn miền  
giá trị của các tham số đầu vào.  
9
Ví dụ 1.4: Mẫu kiểm thử tham số hóa AAAA  
[PexMethod]  
void AssumeActAssert(ArrayList list, object item) {  
// assume  
PexAssume.IsNotNull(list);  
// arrange  
var count = list.Count;  
// act  
list.Add(item);  
// assert  
Assert.IsTrue(list.Count == count + 1);  
}
1.3.5. Lựa chọn đầu vào kiểm thử với Pex  
Thêm tham số vào UT cải thiện đặc tả về hành vi mong muốn nhưng lại mất đi  
các ca kiểm thử cụ thể. Ta cần những giá trị thực sự cho các tham số đầu vào để sinh  
lại các ca kiểm thử cụ thể. PUT sẽ không thể thực thi nếu không có các giá trị cụ thể  
được truyền vào cho các tham số đầu vào của PUT.  
Để có thể sinh các đầu vào cụ thể cho PUT. Pex cần phải phân tích chương trình  
mà PUT kiểm thử. Có 2 kỹ thuật phân tích chương trình đó là phân tích tĩnh và phân  
tích động:  
+ Phân tích tĩnh (static analysis): Kiểm chứng một tính chất nào đó của chương  
trình bằng việc phân tích tất cả các đường đi thực thi. Kỹ thuật này coi các cảnh bảo  
(violations) là các lỗi (error).  
+ Phân tích động (dynamic analysis): Kiểm chứng một tính chất bằng việc phân  
tích một số đường đi thực thi. Đây là một kỹ thuật phân tích động hỗ trợ việc phát hiện  
ra các lỗi (bugs) nhưng không khẳng định được rằng có còn những lỗi khác hay không.  
Các kỹ thuật này thường không tìm ra được tất cả các lỗi.  
Pex cài đặt một kỹ thuật phân tích chương trình bằng cách kết hợp cả hai kỹ thuật  
phân tích chương trình ở trên gọi là thực thi tượng trưng động[14, 25]. Về bản chất  
Pex là một công cụ hỗ trợ kỹ thuật kiểm thử hộp trắng (white-box testing). Tương tự  
như kỹ thuật phân tích chương trình tĩnh, Pex chứng minh được rằng một tính chất  
được kiểm chứng trong tất cả các đường đi khả thi. Pex chỉ báo cáo (reporting) về các  
lỗi thực sự như với kỹ thuật phân tích chương trình động.  
10  
Pex sử dụng khả năng của bộ xử lý ràng buc Z3[31] kết hợp với các lý thuyết  
toán học khác như hàm chưa định nghĩa, lý thuyết mảng, bit-vetor[2] để giải quyết  
ràng buộc sinh ra trong quá trình thực thi tượng trưng động và sinh ra các đầu vào  
kiểm thử cụ thể cho PUT.  
11  
Chương 2: Sinh dữ liệu kiểm thử tự động cho PUT  
Trong kiểm thử phần mềm, các ca kiểm thử thường được tạo ra thủ công do đó  
gây tốn kém trong chi phí phát triển phần mềm và làm kéo dài thời gian để hoàn thành  
một phần mềm.  
Có các phương pháp khác nhau hỗ trợ việc sinh tự động các ca kiểm thử một  
cách có hệ thống giúp giảm chi phí cho việc kiểm thử phần mềm. Một trong những  
phương pháp đơn giản nhất để sinh tự động các ca kiểm thử đó là kiểm thử ngẫu nhiên  
(random testing)[5]. Với kiểm thử ngẫu nhiên các đầu vào cho hệ thống được sinh  
ngẫu nhiên. Để thực hiện, một luồng các bits được sinh ngẫu nhiên để thể hiện cho các  
giá trị của tham số đầu vào. Giả sử với một hàm nhận tham số đầu vào có kiểu string  
thì chỉ cần sinh ngẫu nhiên một luồng các bits để thể hiện giá trị cho một chuỗi. Kiểm  
thử ngẫu nhiên có ưu điểm là dễ dàng sinh các đầu vào ngẫu nhiên và hệ thống kiểm  
thử ngẫu nhiên không yêu cầu nhiều tài nguyên bộ nhớ lúc thực thi. Nhưng hạn chế  
của nó là kiểm tra cùng một hành vị thực thi của chương trình nhiều lần với những đầu  
vào khác nhau và chỉ có thể kiểm tra được một số trường hợp thực thi của chương  
trình. Thêm vào đó, kiểm thử ngẫu nhiên khó xác định được khi nào việc kiểm thử nên  
được dừng lại và nó không biết tại điểm nào không gian trạng thái đã được thám hiểm  
hết. Để xác định khi nào việc kiểm thử dừng lại thì hệ thống kiểm thử ngẫu nhiên được  
kết hợp với các adequacy criterion [5]. Giả sử adequacy criterion là bao phủ lệnh  
(statement coverage)[5] thì việc kiểm thử chỉ dừng lại khi tất cả các câu lệnh của  
chương trình được thực thi ít nhất một lần.  
Một phương pháp khác đang rất phổ biến giúp khắc phục được hạn chế của kiểm  
thử ngẫu nhiên đó là thực thi tượng trưng[6]. Thực thi tượng trưng chính là việc xây  
dựng các ràng buộc dựa vào các giá trị tượng trưng và giải quyết các ràng buộc đó để  
sinh ra các giá trị làm đầu vào chương trình mà có thể thực thi chương trình theo các  
đường đi thực thi khác nhau. Ý tưởng chính của thực thi tượng trưng đó là thực thi một  
chương trình với những giá trị tượng trưng. Có hai cách tiếp cận đối với thực thi tượng  
trưng đó là cách tiếp cận dựa trên phân tích tĩnh và phân tích động chương trình.  
Việc lựa chọn các đầu vào kiểm thử cho PUT liên quan tới vấn đề sinh dữ liệu  
kiểm thử tự động. Sinh dữ liệu kiểm thử tự động là một lĩnh vực nghiên cứu rất rộng  
trong kiểm thử phần mềm. Có rất nhiều các kỹ thuật khác nhau[8, 10, 18] được sử  
dụng để sinh dữ liệu kiểm thử tự động. Tuy nhiên với PUT thì các dữ liệu làm đầu vào  
kiểm thử cần được chọn lựa làm sao để PUT có thể kiểm tra được sự thực thi của  
12  
chương trình theo tất cả các đường đi có thể. Hơn nữa số lượng các đầu vào kiểm thử  
cho PUT là tối thiểu. Việc sinh dữ liệu để có thể kiểm tra được sự thực thi một chương  
trình theo tất cả các đường đi có thể với các dữ liệu đó là một vấn đề phức tạp và  
không phải lúc nào cũng thực hiện được trên thực tế. Thực chất của vấn đề sinh dữ liệu  
kiểm thử tự động cho PUT là việc xây dựng ràng buộc và xử lý ràng buộc. Các ràng  
buộc được xây dựng sao cho chúng có thbiểu thị cho các đường đi thực thi trong một  
chương trình. Và giải quyết các ràng buộc đó sẽ sinh ra các dữ liệu mà chương trình có  
thể thực thi với các dữ liệu đó để đi theo các đường đi khác nhau trong chương trình.  
2.1. Thực thi tượng trưng  
2.1.1. Những khái niệm cơ bản  
Một chương trình P có thể xem xét như một hàm, P : S→ R , trong đó S là tập  
hợp các đầu vào (input) có thể và R là tập hợp các đầu ra (output) có thể. S có thể  
được biểu diễn bởi vector I=(x1,…,xk,…,xn), trong đó xk là tham số đầu vào thk của  
P với k N. Một bộ giá trị i=(d1,...,dk,…,dn) biểu thị cho một đầu vào của P, i S,  
trong đó dk là các giá trị cụ thể sao cho dk Dxk với Dxk là miền giá trị của tham số  
đầu vào xk. Sự thực thi của chương trình P với đầu vào i S được biểu thị bởi P(i).  
Biểu đồ luồng điều khiển (CFG) của một chương trình P là một bộ G=(N, E, s,  
e), trong đó G là một đồ thị có hướng, với N là tập hợp các nút (node), E = {(n,m) |  
n,m N} là tập hợp các cạnh, s là nút vào và e là nút ra, s và e là duy nhất. Mỗi nút  
được định nghĩa như một khối cơ bản (basic block) là một dãy liên tục các chỉ thị(câu  
lệnh) sao cho luồng điều khiển khi đi vào nút và ra khỏi nút không bị ngưng lại (halt).  
Điều này có nghĩa là nếu bất cứ câu lệnh nào của block được thực thi thì toàn bộ block  
được thực thi. Mỗi cạnh của CFG nối 2 nút với nhau và được gán nhãn với một biểu  
thức điều kiện rẽ nhánh. Nếu cạnh không được gán nhãn có nghĩa là điều kiện luôn  
đúng.  
Một đường đi (path) cụ thể là dãy các nút: p=(p1, p2,…,pn) với pn là nút cuối của  
đường đi p và (pi,pi+1) E (1 < i < n-1). Nếu tồn tại i S sao cho sự thực thi P(i) đi  
theo đường đi p thì p gọi là đường đi khả thi, ngược lại p là đường đi không khả thi.  
Một đường đi bắt đầu tại nút vào và kết thúc tại nút ra gọi là đường đi đầy đủ, nếu kết  
thúc tại nút không phải là nút ra thì gọi là đường đi không đầy đủ (path segment).  
Một chương trình P cũng có thể xem gồm tập hợp các câu lệnh (statements) là  
thành phần nhỏ nhất trong một chương trình mà có thể được thực thi riêng r. Bằng  
việc thực thi một câu lệnh chương trình có thể chuyển đổi trạng thái thực thi của nó từ  
13  
trạng thái hiện thời tới trạng thái mới. Một đường đi thực thi của chương trình P là một  
dãy các câu lệnh mà có thể được thực thi theo thứ tự từ điểm bắt đầu của chương trình.  
Đoạn đường đi đầu tiên (path prefix) có độ dài n của đường đi thực thi p là một dãy  
bao gồm n câu lệnh đầu tiên của p.  
Do đó việc sinh dữ liệu kiểm thử cho PUT là việc sinh một tập hợp tối thiểu các  
đầu vào i Є S sao cho có thể thực thi tất cả các đường đi trong CFG của chương trình  
được PUT kiểm thử.  
2.1.2. Thực thi tượng trưng tĩnh  
Ý tưởng chính của thực thi tượng trưng (SE)[6] là thực thi chương trình với các  
giá trị tượng trưng (symbolic values) thay vì các giá trị cụ thể (concrete values) của  
các tham số đầu vào.  
Với mỗi tham số đầu vào một giá trị tượng trưng được đưa ra để kết hợp với nó.  
Mỗi biến trong chương trình P mà giá trị của nó phụ thuộc vào giá trị của các tham số  
đầu vào thì trong quá trình thực thi tượng trưng một giá trị tượng trưng sẽ được tính  
toán để kết hợp cùng với nó. Mỗi giá trị tượng trưng biểu thị cho một tập hợp các giá  
trị cụ thể mà một biến hoặc một tham số đầu vào có thể nhận. Kết quả trả về của một  
chương trình được thực thi tương trưng nếu có cũng được biểu thị bởi biểu thức của  
các giá trị tượng trưng.  
Giá trị tượng trưng ca biến x có thể được biểu thị bởi:  
(a). Một ký hiệu đầu vào(input symbol).  
(b). Một biểu thức kết hợp giữa các giá trị tượng trưng bởi các toán tử.  
(c). Một biểu thức kết hợp giữa giá trị tượng trưng và giá trị cụ thể bởi  
toán tử.  
Một ký hiệu đầu vào biểu thị cho giá trị tượng trưng của một tham số đầu vào lúc  
bắt đầu thực thi chương trình. Các tham số đầu vào khác nhau của P được biểu thị bởi  
các ký hiệu đầu vào khác nhau. Các toán tử (operator) là các phép toán như cộng (+),  
tr(), nhân (*), chia (/).  
Nếu giá trị của một biến x không phụ thuộc vào các giá trị đầu vào thì không có  
giá trị tượng trưng nào được tính toán để kết hợp với nó. Giá trị tượng trưng cuả các  
biến và các tham số đầu vào được cập nhật như các giá trị cụ thể của nó trong quá trình  
thực thi. Gán một giá trị cụ thể từ một biến tới biến khác dẫn đến giá trị tượng trưng  
cũng được sao chép nếu biến được gán tới một biến khác có một giá trị tượng trưng.  
14  
Giả sử với một câu lệnh gán x=e, nếu e là một tham số đầu vào, thì giá trị tượng trưng  
được gán cho x sẽ có dạng (a). Nếu e là một biểu thức tính toán gồm các toán hạng.  
Các toán hạng đó có thể là biến, tham số đầu vào hoặc hằng thì giá trị tượng trưng của  
biến x sẽ là một biểu thức tượng trưng dạng (b) nếu mỗi toán hạng trong biểu thức có  
một giá trị tượng trưng kết hợp với nó, hoặc là một biểu thức tượng trưng dạng (c) nếu  
có toán hạng là hằng số hoặc không có giá trị tượng trưng kết hợp với nó. Giá trị cụ  
thể của một hằng hoặc một biến cũng được sử dụng trong biểu thức tượng trưng nếu  
như hằng hoặc biến đó không có giá trị tượng trưng kết hợp với nó.  
Trạng thái của một chương trình được thực thi tương trưng bao gồm các giá trị  
của các biến trong chương trình, điều kiện đường đi (PC) và biến đếm chương trình  
(program counter). Biến đếm chương trình xác định chỉ thị (câu lệnh) tiếp theo sẽ được  
thực thi. Mỗi PC là một biểu thức kết hợp bởi các ràng buộc mà các giá trị đầu vào  
chương trình cần thỏa mãn để chương trình được thực thi theo đường đi tương ứng với  
PC đó. Mỗi ràng buộc là một biểu thức tượng trưng dạng x o y trong đó x là giá trị  
tượng trưng, y là giá trị tượng trưng hoặc giá trị cụ thể và o  {, , =, <, >, ≥}. Các  
ràng buộc đó chính là biểu thức của điều kiện rẽ nhánh và biểu thức phủ định của điều  
kiện rẽ nhánh tương ứng với nhánh true và nhánh false. Tại mỗi câu lệnh rẽ nhánh, các  
ràng buộc được tạo ra. Các ràng buộc này được biểu thị bởi biểu thức của các giá trị  
tượng trưng hay biểu thức của giá trị tượng trưng và giá trị cụ thể phụ thuộc vào biến  
xuất hiện trong biểu thức điều kiện của câu lệnh rẽ nhánh có giá trị tượng trưng được  
tính toán để kết hợp với nó hay không.  
Trong quá trình thực thi tượng trưng, việc chương trình được thực thi theo một  
đường đi cụ thể nào đó không phụ thuộc vào các giá trị cụ thể của các biến và các  
tham số đầu vào. Tại các điểm rẽ nhánh, cả hai nhánh ra sẽ được xem xét để điều  
hướng sự thực thi hiện thời đi theo. SE chủ yếu liên quan tới việc thực thi hai loại câu  
lệnh đó là câu lệnh gán (assignment statments) và câu lệnh rẽ nhánh. Tại các câu lệnh  
gán thì giá trị tượng trưng của các biến chương trình cũng như các tham số đầu vào có  
liên quan tới câu lệnh gán đó được cập nhật. Tại các câu lệnh rẽ nhánh, chương trình  
sẽ được điều hướng để thực thi theo cả hai nhánh. Và hai ràng buộc đường đi tương  
ứng với hai nhánh sẽ được tạo ra. Một ràng buộc là biểu thức điều kiện tại câu lệnh rẽ  
nhánh. Còn ràng buộc kia là phủ định của biểu thức điều kiện rẽ nhánh. Các ràng buộc  
này sẽ được cập nhật vào điều kiện đường đi tương ứng với các nhánh đó. Đồng thời  
các PC này sẽ được đánh giá để xác định đường đi tương ứng với PC đó có khả thi.  
Nếu PC được đánh giá trở thành false thì SE sẽ quay lui và chỉ thực thi theo nhánh khả  
15  
thi. Các PC được tạo ra bằng cách thu gom các ràng buộc trên các đường đi tương ứng  
và giải quyết các ràng buộc này sẽ sinh ra các giá trị cụ thể cho các tham số đầu vào.  
Để mô tả sự thực thi tượng trưng một chương trình. Một cây thực thi tượng trưng  
(SET) được đưa ra để biểu thị cho các đường đi thực thi trong quá trình thực thi tượng  
trưng một chương trình. Các nút biểu thị cho các trạng thái của chương trình được thực  
thi tượng trưng và các cạnh biểu thị cho sự chuyển đổi trạng thái từ trạng thái này sang  
trạng thái khác.  
Ví dụ 2.1:  
public void Swap(int x, int y){  
1:  
2:  
3:  
4:  
5:  
6:  
}
if (x > y) {  
x = x + y;  
y = x - y;  
x = x - y;  
if (x - y > 0)  
assert(false);  
Hình 2 : Cây thực thi tượng trưng  
Cây thực thi tượng trưng (Hình 2) biểu thị cho việc thực thi tượng trưng hàm  
Swap. Bắt đầu thực thi tượng trưng hàm Swap bằng cách gán giá trị tượng trưng cho  
các tham số đầu vào x và y lần lượt là X và Y, đồng thời khởi tạo PC là true. Tới câu  
lệnh rẽ nhánh 1, cả hai nhánh đi của chương trình đều chọn để thực thi với các giá trị  
tượng trưng. Tại câu lệnh này, biu thức điều kiện rẽ nhánh và biểu thức phủ định của  
điều kiện rẽ nhánh được thêm vào PC theo các nhánh tương ứng. Trong thực thi tượng  
16  
trưng, nếu điều kiện rẽ nhánh được thêm vào PC thì PC đó tương ứng với PC của  
nhánh mà điều kiện rẽ nhánh nhận giá trị true. Sau khi thực thi câu lệnh 1, hàm Swap  
tiếp tục được thực thi theo nhánh mà điều kiện rẽ nhánh ở câu lệnh 1 nhận giá trị true.  
Khi thực thi các câu lệnh gán 2, 3, 4 thì giá trị của các biến được cập nhật với giá trị  
mới. Khi tới câu lệnh rẽ nhánh 5, thêm 2 nhánh đi mới được xem xét để thực thi với  
các giá trị tượng trưng. PC tiếp tục được cập nhật theo các nhánh tương ứng.  
Tại đây, PC được cập nhật với điều kiện rẽ nhánh ở 5 trở thành false do không  
tồn tại bộ giá trị nào thỏa mãn PC. Vì vậy hàm Swap chỉ thực thi theo nhánh mà PC  
được cập nhật với biểu thức phủ định của điều kiện rẽ nhánh tại 5. Và câu lệnh 6 sẽ  
không bao giờ được thực thi.  
Tại mỗi điểm rẽ nhánh, PC được cập nhật và một bộ xử lý ràng buộc được sử  
dụng để xác định nhánh tương ứng với PC đó có khả thi hay không để điều hướng việc  
thực thi hiện thời đi theo nhánh đó . Nếu PC được đánh giá tới false thì SE sẽ quay lui  
và chỉ thực thi chương trình theo nhánh mà PC được đánh giá tới true.  
2.1.3. Thực thi tượng trưng động  
Thực thi tượng trưng động[14, 25] là kỹ thuật thực thi tương trưng dựa trên phân  
tích chương trình động. Thực thi tượng trưng động chính là sự kết hợp giữa thực thi cụ  
thể và thực thi tượng trưng. Trong thực thi tượng trưng động, chương trình được thực  
thi nhiều lần với những giá trị khác nhau của tham số đầu vào.  
Bắt đầu bằng việc chọn những giá trị tùy ý cho các tham số đầu vào và thực thi  
chương trình với những giá trị cụ thể đó. Với những giá trị cụ thể này thì chương trình  
sẽ được thực thi theo một đường đi xác định. Thực thi chương trình với các giá trị cụ  
thể của tham số đầu vào và thu gom các ràng buộc trong quá trình thực thi theo đường  
đi mà sự thực thi cụ thể này đi theo, đồng thời suy ra các ràng buộc mới từ những ràng  
buộc đã thu gom được.  
Tại các câu lệnh rẽ nhánh, biểu thức điều kiện rẽ nhánh sẽ được đánh giá phụ  
thuộc vào các giá trị cụ thể của các tham số đầu vào. Nếu biểu thức điều kiện rẽ nhánh  
nhận giá trị là True thì biểu thức của điều kiện rẽ nhánh sẽ được thu gom vào ràng  
buộc của PC và được ghi nhớ, đồng thời phủ định của điều kiện rẽ nhánh sẽ được sinh  
ra và được thêm vào một PC tương ứng với nhánh còn lại mà sự thực thi cụ thể đó  
không đi theo. Một bộ xử lý ràng buộc (Constraint Solver) sẽ được sử dụng để giải  
quyết các ràng buộc mới sinh ra này để sinh ra các giá trị cụ thể của tham số đầu vào.  
Ngược lại, nếu là False thì biểu thức phủ định của điều kiện rẽ nhánh sẽ được thu gom  
17  
vào ràng buộc của PC tương ứng với nhánh đi mà sự thực thi hiện thời đang đi theo và  
được ghi nhớ. Đồng thời điều kiện rẽ nhánh sẽ được sinh ra và thêm vào PC tương ứng  
với nhánh đi còn lại mà sự thực thi hiện thời không đi theo. Các giá trị mới được sinh  
ra của các tham số đầu vào sẽ tiếp tục được thực thi và quá trình này sẽ được lặp lại  
cho tới khi chương trình được thực thi theo tất cả các đường đi.  
Do các chương trình được thực thi với những giá trị cụ thể nên có thể thấy rằng  
tất cả các đường đi phân tích được trong quá trình thực thi tượng trưng động đều là các  
đường đi khả thi.  
Thuật toán 2.1: DSE  
S : Tập hợp tất cả các câu lệnh của chương trình P  
S : Tập con của S (s S)  
I : Tập hợp các đầu vào của P  
P(i): Thực thi chương trình với đầu vào i I, sao cho s được thực thi  
J : Tập hợp các đầu vào của P được thực thi (J={i | P(i)})  
C(i): Ràng buộc thu gom được từ việc thực thi P(i), hay còn gọi là điều kiện đường đi  
C’(i): Điều kiện đường đi suy ra từ C(i)  
Bước 0: J:= {} (tập rỗng)  
Bước 1: Chọn đầu vào i không thuộc J (dừng lại nếu không có i nào được tìm ra)  
Bước 2: Output i  
Bước 3: Thực thi P(i); ghi nhớ điều kiện đường đi C(i), suy ra C’(i)  
Bước 4: Đặt J := J i  
Bước 5: Quay lại bước 1  
Ví d2.2:  
0:void DSE(int[] a)  
1:{  
2:  
3:  
4:  
5:  
6:}  
if (a == null) return;  
if (a.length > 0)  
if (a[0] == 123456789)  
throw new Exception("bug");  
Solve (c): Biểu thị cho việc xử lý ràng buộc c. Hàm DSE bắt đầu được thực thi  
với giá trị tùy ý của tham số đầu vào. Giá trị tùy ý này thường được chọn là giá trị mặc  
định tương ứng với kiểu của tham số đầu vào. Ở đây giá trị null được chọn, sau đó  
thực thi hàm DSE với giá trị null này. Khi tới câu lệnh rẽ nhánh 2, điều kiện a==null  
được thỏa mãn do đó ràng buộc C(i):(a==null) được ghi nhớ. Ràng buộc C’(i) được  
suy ra bằng cách lấy phủ định của điều kiện rẽ nhánh, C’(i): (a!=null). Solve (C’(i))  
suy ra được a={}.  
18  
Tiếp tục thực thi chương trình với giá trị a={}. Với a={} khi tới câu lệnh rẽ  
nhánh 3, biểu thức a.length > 0 nhận giá trị false, ràng buộc C(i): (a!=null) &&  
!(a.length >0) được ghi nhớ. Ràng buộc C’(i):(a!=null && a.length > 0) được sinh ra,  
solve (C’(i)) ta được giá trị mới của tham số đầu vào là {0}.  
Tiếp tục thực thi hàm DSE với giá trị a={0}. Với giá trị a={0} khi đi tới câu lệnh  
rẽ nhánh 4, thì biểu thức điều kiện rẽ nhánh nhận giá trị false đo đó ràng buộc C’(i):(  
a!=null && a.length>0 && a[0]!=123456789) được ghi nhớ. Điều kiện C’(i): (a!=null  
&& a.length>0 && a[0]==123456789) được sinh ra, solve (C’(i)) ta được giá trị  
a={12345678}. Đến đây thì không còn đường đi nào của hàm DSE mà chưa được thực  
thi và qua trình thực thi sẽ được dừng lại.  
Bảng 1: Ví dụ về thực thi tượng trưng động  
Formatted: Indent: First line: 0 pt  
Formatted: Indent: First line: 0 pt  
Formatted: Indent: First line: 0 pt  
Ràng buộc C’(i)  
Input i  
null  
Ràng buộc được ghi nhớ C(i)  
a = = null  
a!=null  
{}  
a!=null && !(a.length>0)  
a!=null && a.length>0  
&& a[0]!=123456789  
a!=null && a.length>0  
&& a[0]!=123456789  
Formatted: Indent: First line: 0 pt  
Formatted: Indent: First line: 0 pt  
a!=null&& a.length>0  
{0}  
a!=null && a.length>0  
&& a[0]!=123456789  
{123456789}  
Một số hệ thống sinh kiểm thử tự động cài đặt DSE bằng cách khởi tạo một cây  
thực thi tương trưng để biểu thị các đường đi thực thi khác nhau. Nếu như có thể xây  
dựng được một cây thực thi tượng trưng đầy đủ thì có thể sinh ra các giá trị đầu vào  
sao cho có thể đạt được sự bao phủ luồng điều khiển (CFG coverage)[5] ở mức cao.  
Với những hệ thống này, mã nguồn chương trình cần được sửa đổi để cho phép  
thực thi tượng trưng được thực hiện dọc theo việc thực thi cụ thể. Chương trình được  
thực thi với những giá trị ngẫu nhiên với một chiều sâu (depth) định trước của SET.  
Chiều sâu được sử dụng để giúp cho việc thực thi một chương trình được dừng lại  
trong trường hợp chương trình có vòng lặp vô tận hoặc các hàm đệ quy. Khi chương  
trình bắt đầu được thực thi thì một cây thực thi tương trưng tương ứng cũng được khởi  
tạo. Trong quá trình thực thi, các giá trị tượng trưng của các biến sẽ đươc tính toán và  
các ràng buộc đường đi sẽ được sinh ra từ các giá trị tượng trưng đó. Với mỗi ràng  
buộc được sinh ra thì SET sẽ được thêm vào một đỉnh (node) tương ứng với ràng buộc  
đó. Việc xây dựng SET tiến hành trong suốt quá trình thực thi. Có thể xem mỗi lần  
thực thi là mỗi lần duyệt một đường đi của SET.  
19  
Khi các câu lệnh rẽ nhánh được thực thi, các đường đi trong SET cũng được mở  
rộng theo các nhánh đó. Với những giá trị cụ thể việc thực thi sẽ đi theo một nhánh cụ  
th. Nhánh còn lại mà sự thực thi cụ thể đó không đi theo sẽ có một đỉnh mới được tạo  
ra và được đánh dấu là chưa được thăm. Đỉnh mới được tạo ra sẽ lưu các ràng buộc  
tương ứng với nhánh mà đỉnh đó được thêm vào. Sau mỗi lần thực thi, một đỉnh mà  
chưa được thăm trong SET sẽ được chọn và ràng buộc đường đi tương ứng với đoạn  
đường đi từ đỉnh gốc (root) của SET tới đỉnh được chọn đó sẽ được thu gom và ràng  
buộc đường đi này sẽ được đưa tới một bộ xử lý ràng buộc (Constraint Solver) để xử  
lý. Nếu ràng buộc đường đi này không thỏa mãn, một đỉnh khác của SET mà chưa  
được thăm sẽ được chọn, còn nếu ràng buộc đường đi này thỏa mãn thì bộ xử lý ràng  
buộc sẽ sinh ra các giá trị đầu vào cụ thể cho lần thực thi tiếp theo. Khi các đỉnh tương  
ứng với các nhánh trong SET đều được thăm hết thì thuật toán sẽ tạm dừng  
(terminate). Và các giá trị đầu vào cụ thể được sinh ra cùng với những thông tin phân  
tích được trong mỗi lần thực thi (method summary) sẽ được sử dụng để sinh ra các UT  
và cho mục đích báo cáo. Các giá trị đầu vào cụ thể được trả về sau mỗi lần chạy được  
kết hợp với các thông tin về lỗi phát hiện được nếu lần thực thi đó chương trình ngưng  
lại vì một ngoại lệ chưa được xử lý, hoặc vì một lỗi nào đó.  
Ví dụ 2.3: Thực thi tượng trưng với phương thức nhận đầu vào là đối tượng  
Ta xét một phương thức nhận 2 tham số đầu vào, một tham số là đối tượng  
SimpleList và một tham số có kiểu int:  
Class SimpleList  
{
public int value ;  
public List next ;  
.../* other methods */  
}
20  
1: void Simple(SimpleList o, int x){  
2:  
3:  
4:  
5:  
6:  
7:  
8:  
9:}  
x=x+5;  
if(x > 0)  
o.value=x;  
else  
o=null;  
if(o.next==o)  
error();  
1 void Simple(SimpleList o, int x){  
2
3
x=x+5;  
if(x > 0)  
4
5
o.value=x;  
else  
6
7
o=null;  
if(o.next==o)  
8
9}  
error();  
Với những phương thức nhận tham số đầu vào có kiểu đối tượng thì kỹ thuật  
khởi tạo lười[29] được sử dụng trong việc thực thi tượng trưng phương thức đó. Giả sử  
các giá trị ngẫu nhiên được chọn là x=-10, o=null. Các giá trị tượng trưng được gán  
tương ứng tới các tham số đầu vào cùng với những giá trị cụ thể được chọn ở trên.  
S(x)=sym, R(o)=obj1. Thực thi chương trình với giá trị của các tham số đầu vào được  
chọn ngẫu nhiên này. Sau khi thực thi câu lệnh 2, giá trị tượng trưng của x là  
S(x)=sym+5, giá trị cụ thể của nó cũng được tính toán (x = -5). Tới câu lệnh rẽ nhánh  
3, với giá trị x= -5 thì điều kiện rẽ nhánh được đánh giá tới false. Ràng buộc  
S(x)=sym+5 <= 0 lưu vào đỉnh tương ứng của SET cho nhánh mà sự thực thi cụ thể đi  
theo và được đánh dấu là đã được thăm, đồng thời một đỉnh mới đại diện cho nhánh  
còn lại được tạo ra và ràng buộc sym+5 > 0 được lưu vào đỉnh này, đỉnh mới được tạo  
ra được đánh dấu là chưa được thăm. Đến câu lệnh 7 thì một ngoại lệ  
NullPointerException được ném ra và việc thực thi sẽ dừng lại. Cây thực thi tượng  
trưng được xây dựng tương ứng với lần chạy này (Hình 3(a)). Đỉnh mới được tạo ra  
của SET mà chưa được thăm được chọn để thu gom ràng buộc trên nhánh tương ứng  
với đỉnh chưa được thăm đó. Ràng buộc thu gom được là sym+5 > 0. Giải quyết ràng  
buộc này ta được một giá trị cụ thể của x giả sử là x=0. Do không có ràng buộc liên  
quan tới đối tượng l được tạo ra nên giá trị của nó sẽ được khởi tạo với phương thức  
khởi tạo mặc định cho lần thực thi tiếp theo.  
Sau khi khởi tạo đối tượng o và giải quyết ràng buộc, chương trình tiếp tục được  
thực thi với các giá trị đầu vào mới này đó là x=0, o=new simpleList(). Đỉnh chưa  
21  
được thăm tương ứng với nhánh thực thi này tạo ra từ lần thực thi trước trong SET sẽ  
được chọn để mở rộng. Đến câu lệnh 4, trường value của đối tượng o được truy cập  
lần đầu tiên. Một giá trị tượng trưng sẽ được kết hợp với nó, và giá trị tượng trưng này  
được cập nhật do giá trị của trường đó được gán với giá trị của biến x. Tới câu lệnh 7,  
một đối tượng o.next sẽ được khởi tạo lười với một giá trị cụ thể và một giá trị tượng  
trưng kết hợp với nó, o.next=new simpleList(), R(o.next)=obj2. Tại câu lệnh rẽ nhánh  
này, 2 ràng buộc tương ứng với 2 nhánh được tạo ra obj1=obj2, và obj1≠obj2. Ràng  
buộc obj1≠obj2 tương ứng với nhánh mà sự thực thi cụ thể hiện thời đang đi theo. Với  
ràng buộc obj1=obj2 thì một đỉnh mới được tạo ra tương ứng với nhánh mà sự thực thi  
cụ thể hiện thời không đi theo để lưu ràng buộc obj1=obj2 đỉnh mới này được đánh  
dấu là chưa được thăm. Cây thực thi tượng trưng tương ứng với lần thực thi này được  
biểu thị trong Hình 3(b). Sau khi lần chạy này kết thúc đỉnh mới được tạo ra này lại  
được chọn để mở rộng và ràng buộc thu gom được trên đường đi chứa đỉnh này là  
S(x)=sym+5 > 0 && obj1=obj2 && S(o.value)= sym+5. Ràng buộc đối tượng  
obj1=obj2 được giải quyết bằng phép gán tham chiếu giữa 2 đối tượng mà các giá trị  
tượng trưng này đại diện, o.next=o. Đối tượng o lúc này được khởi tạo và giá trị của  
trường value được gán với giá trị sinh ra từ việc giải quyết ràng buộc. Cây thực thi  
tương trưng tương ứng với lần thực thi chương trình với các giá trị đầu vào mới sinh ra  
này được biểu thị trong Hình 3(c).  
Hình 3: Cây thực thi tượng trưng được quản lý riêng  
22  

Tải về để xem bản đầy đủ

pdf 69 trang yennguyen 17/06/2025 430
Bạn đang xem 30 trang mẫu của tài liệu "Khóa luận Sinh ca kiểm thử tham số hóa cho chương trình Java", để tải tài liệu gốc về máy hãy click vào nút Download ở trên.

File đính kèm:

  • pdfkhoa_luan_sinh_ca_kiem_thu_tham_so_hoa_cho_chuong_trinh_java.pdf