Kiểm thử hộp trắng dựa trên đồ thị luồng điều khiển nhằm tạo ra dữ liệu thỏa mãn tiêu chí bao phủ các thành phần trên đồ thị, nhƣ các đỉnh, các cung hay các lộ trình. Tùy theo tiêu chí bao phủ, chúng ta phải xác định các lộ trình kiểm thử đƣợc thực thi. Từ đó, các ca kiểm thử đƣợc tạo ra để thực thi các lộ trình tƣơng ứng.
Trong mục này, học viên sẽ trình bày chi tiết các tiêu chí bao phủ cấu trúc dựa trên đồ thị luồng điều khiển. Quy trình chung áp dụng kiểm thử dựa trên đồ thị luồng điều khiển để tạo ra các ca kiểm thử đƣợc minh họa trong Hình 2.2
Hình 2. 3 – Quy trình tạo ca kiểm thử dựa trên đồ thị luồng điều khiển Tiêu chí bao phủ đỉnh
Tiêu chí bao phủ đỉnh yêu cầu thực thi các lộ trình kiểm thử sao cho tất cả các đỉnh trong đồ thị đƣợc thực thi. Tiêu chí bao phủ đỉnh dựa trên khái niệm thực thi tất cả các câu lệnh trong một chƣơng trình.
Mỗi tiêu chí bao phủ đƣợc đánh giá bởi tỉ lệ bao phủ: tỉ lệ giữa số các đối tƣợng (đỉnh, cung, lộ trình,…) đƣợc bao phủ trên số các đối tƣợng mà tiêu chí yêu cầu phải bao phủ. Tỉ lệ bao phủ nhằm đánh giá mức độ thỏa mãn của tiêu chí đặt ra.
Ví dụ: Xét hàm sum để tính tổng của hai số nguyên: int sum (int a, int b)
{ int s; if (a = = 0) s = a; else s= a + b; return s; }
Đối với chƣơng trình này, ca kiểm thử {a = 0, b ≠ 0 }sẽ phát hiện lỗi. Đồ thị luồng điều khiển biểu diễn chƣơng trình sum Hình 2.3
Chƣơng trình
Xây dựng đồ thị
luồng điều khiển Đồ thị luồng điều khiển Chọn lộ trình kiểm thử Tiêu chí bao phủ Các lộ trình kiểm thử Tạo các ca kiểm thử Ca kiểm thử
Hình 2. 4 – Đồ thị luồng điều khiển biểu diễn chương trình sum
Đối với đồ thị Hình 2.3, nếu chúng ta chọn dữ liệu thử { a = 5, b = 9}, thì lộ trình kiểm thử [n0, n1, n3] đƣợc thực thi. Khi đó, tỉ lệ bao phủ đối với tiêu chí bao phủ đỉnh là: ¾ = 75%, trong khi thực thi lộ trình [n0, n2 n3] mới có thể phát hiện lỗi.
Khi tỉ lệ bao phủ bằng 1, thì tiêu chí bao phủ đỉnh đƣợc thỏa mãn, tất cả các đỉnh của đồ thị đƣợc bao phủ, nghĩa là tất cả các câu lệnh trong chƣơng trình đều đƣợc thực thi.
Tiêu chí bao phủ cung
Thực tế, tiêu chí bao phủ đỉnh không đủ hiệu quả để phát hiện các lỗi lập trình. Xét đoạn chƣơng trình trong Hình 2.4
scanf(“%d”, &a); if (a!=0) a=a + 1; s = 1/a; n 1 n 0 n 2 n 3 a= a + 1 a ≠ 0 a == 0 s= 1/a n 0 n 1 n 2 n 3 s== a + b a ≠ 0 a == 0 s== a return s
Trong trƣờng hợp này, chúng ta chọn dữ liệu {a = 3, thì lộ trình kiểm thử [n0,
n1, n2, n3] đƣợc thực thi và bao phủ tất cả các đỉnh của đồ thị. Tuy nhiên, lỗi (chia cho 0) không đƣợc phát hiện.
Tiêu chí bao phủ cung yêu cầu tất cả các cung phải đƣợc bao phủ. Tiêu chí này nhằm thực thi không chỉ mọi lệnh mà còn mọi rẽ nhánh trong chƣơng trình. Đối với đồ thị trong Hình 2.4, các cung cần đƣợc bao phủ gồm [n0, n1], [n1, n2], [n0, n2], [n2, n3].
Muốn thỏa mãn tiêu chí bao phủ cung, dữ liệu thử phải thực thi hai lộ trình kiểm thử [n0, n1, n2, n3] và [n0, n2, n3]. Chẳng hạn, chọn các dữ liệu thử gồm {a = 1} và { a = 0}. Khi đó, lỗi chia cho 0 cũng sẽ đƣợc phát hiện.
Nhƣ vậy, tiêu chí bao phủ cung tƣơng đƣơng với việc bao phủ tất cả các giá trị (đúng/sai) của mỗi đỉnh quyết định, nghĩa là tất cả các rẽ nhánh của chƣơng trình.
Ngoài ra, dễ dàng nhận thấy rằng nếu tiêu chí bao phủ cung thỏa mãn thì tiêu chí bao phủ đỉnh cũng đƣợc thỏa mãn.
Tiêu chí bao phủ lộ trình
Tiêu chí bao phủ cung không thể phát hiện lỗi lập trình liên quan đến thực thi chƣơng trình có chứa vòng lặp. Ví dụ, xét đoạn chƣơng trình tính tổng nghịch đảo các phần tử giữa vị trí left, right của mảng a gồm các số nguyên dƣơng.
Scanf ( “%d%d”, &left, &right); i = left; s = 0; while (i < = right) { s = s + a[i]; i = i + 1; } printf (“%f”, 1.0/s);
Đồ thị luồng điều khiển tƣơng ứng đƣợc trình bày trong Hình 2.7
Hình 2. 6 - Đồ thị biểu diễn chương trình tính tổng nghịch đảo
Nếu chọn dữ liệu { a[1] = 2, left = 1, right = 1} thì lộ trình kiểm thử đƣợc thực thi là [c1, c2, c3, c4]. Lộ trình này đảm bảo tiêu chí bao phủ cung cũng nhƣ tiêu chí bao phủ đỉnh, tuy nhiên không phát hiện đƣợc lỗi khi left > right. Khi vòng lặp thực thi, biến s bằng 0 và nghịch đảo của biến s là không thể thực hiện.
Tiêu chí bao phủ lộ trình yêu cầu tất cả các lộ trình phải đƣợc bao phủ . Nếu chúng ta chọn thêm bộ dữ liệu { a[1] = 2, left = 2, right = 1} thì lộ trình [c1, c4] đƣợc thực thi và lỗi chia không sẽ đƣợc phát hiện.
Tiêu chí bao phủ lộ trình độc lập
Đối với chƣơng trình có vòng lặp, số lộtrình là vô hạn. Tuy nhiên, tồn tại tập con nhỏ nhất các lộ trình cho phép tạo ra tất cả các lộ trình còn lại. Tập con nhỏ nhất các lộ trình đó đƣợc gọi là tập các lộ trình độc lập. Hai lộ trình đƣợc gọi là độc lập nếu lộ trình này chứa cung mà lộ trình kia không chứa hoặc ngƣợc lại.
Hơn nữa, McCabe [8] chỉ ra rằng số lộ trình độc lập của một đồ thị luồng điều khiển G, đƣợc kí hiệu v(G), bằng số điều kiện cộng một. Nghĩa là, đối với đồ thị trong Hình 2.7, chỉ có một đỉnh quyết định, vì vậy số lộ trình độc lập v(G) bằng 1 + 1 = 2.
Tiêu chí bao phủ lộ trình độc lập yêu cầu bao phủ đúng v(G) lộ trình độc lập. c1 c2 c4 c3 n 0 n 2 n 3 s= s + a[i] i = i +1 i > right printf (1/s) n 1 i < = right
Scanf(left, right), i= left, s=0
chƣa đƣợc kiểm thử. Ngƣợc lai, nếu chúng ta bao phủ số lộ trình nhiều hơn v(G), thì sự kiểm thử sẽ dƣ thừa về mặt lộ trình độc lập.
Các bƣớc xác định dữ liệu thỏa mãn tiêu chí bao phủ lộ trình độc lập nhƣ sau: Bƣớc 1: Xác định số lộ trình độc lập v(G) bằng cách đếm số quyết định trong chƣơng trình.
Bƣớc 2: Chọn một dữ liệu bất kì. Thông thƣờng chúng ta chọn dữ liệu thử nhằm thực thi lộ trình bao phủ số lƣợng đỉnh quyết định lớn nhất trong đồ thị.
Bƣớc 3: Chọn dữ liệu thử thay đổi giá trị logic của đỉnh quyết định đầu tiên trong lộ trình kết quả của Bƣớc 2, nghĩa là chọn cung khác của đỉnh quyết định đó. Lặp lại Bƣớc 3 cho đến khi tất cả các đỉnh quyết định đƣợc thay đổi giá trị.
Ví dụ 1: Xét chƣơng trình biểu diễn hàm abc:
int abc () { char kytu; int ketqua = 0; kytu = getchar (); if (kytu = = „a‟) { kytu = getchar ();
while ((kytu = = „b‟) || (kytu = = „c‟)) kytu = getchar ();
if (kytu = = „x‟) ketqua = 1;
}
return ketqua; }
Hàm abc nhằm nhận biết các chuỗi kí tự (đƣợc nhập vào từng kí tự) bắt đầu bởi kí tự a theo sau các kí tự b hoặc c và kết thúc bởi kí tự x. Nếu chuỗi kí tự đúng, hàm trả về giá trị 1, ngƣợc lại trả về giá trị 0. Đồ thị luồng điều khiển đƣợc biểu diễn hàm abc đƣợc trình bày trong ở Hình 2.8
Hình 2. 7 - Đồ thị luồng điều khiển biểu diễn hàm abc
Bƣớc 1: Chƣơng trình có hai lệnh if và một lệnh while với hai điều kiện (tƣơng ứng với các đỉnh n1, n3, n 5, n6), vậy v(G) = 4 + 1 = 5. Chúng ta cần bao phủ 5 lộ trình độc lập.
Bƣớc 2: Chọn dữ liệu thử DT1 = {abcx}. Lộ trình đƣợc thực thi là: lt1 = [ n0, n1, n2, n3, n4, n3, n5, n4, n3, n5, n6, n7, n8] n 0 n 1 n 2 ketqua = 1 ketqua = 0; Kytu = getchar (); return ketqua; kytu = = „a‟ n 3 kytu = getchar (); n 4 kytu = = „b‟ n 5 kytu ! = „b‟ kytu = = „c‟ n 6 kytu ! = „c‟ n 7 kytu = = „x‟ n 8 kytu ! = „x‟ kytu = getchar (); kytu ! = „a‟
Bƣớc 3:
+ Chọn đỉnh quyết định đầu tiên trên lộ trình lt1,đó là đỉnh n1. Tại đỉnh này, lộ trình lt1đã bao phủ cung [n1, n2], vậy chúng ta thay đổi và chọn cung [n1, n8]. Để thực hiện điều đó, chọn dữ liệu DT2 = {x}, khi đó lộ trình đƣợc thực thi tƣơng ứng là:
lt2= [ n0, n1, n8]
Lộ trình lt2chứa cung [n1, n8], trong khi lộ trình lt1không chứa cung này. Vậy, hai lộ trình lt1và lt2là độc lập.
+ Tiếp tục trên lộ trình lt1, chọn đỉnh quyết định thứ hai, đó là đỉnh n3. Tại đỉnh này, chọn cung [n3, n5]. Chúng ta chọn dữ liệu là DT3 = {acx}. Lộ trình đƣợc thực thi là:
lt3 = [ n0, n1, n2, n3, n5, n4, n3, n5, n6, n7, n8]
+ Tiếp tục chọn đỉnh quyết định thứ ba trên lộ trình lt1(lƣu ý rằng nếu lộ trình
lt1không còn đỉnh quyết định, thì chọn các đỉnh trên lộ trình lt2và lt3 ), đó là đỉnh n5. Chúng ta chọn cung [n5, n6]. Chúng ta chọn dữ liệu là DT4 = {ax}. Lộ trình đƣợc thực thi là:
lt4 = [ n0, n1, n2, n3, n5, n6, n7, n8]
+ Chọn đỉnh cuối cùng là n6.Chúng ta chọn cung [n6, n8] và dữ liệu DT5= {abc}. Lộ trình đƣợc thực thi là:
lt5 = [ n0, n1, n2, n3, n4, n3, n5, n4, n3, n5, n6, n8]
Dễ dàng nhận thấy 5 lộ trình là độc lập với nhau. Chúng ta đã xác định đầy đủ 5 lộ trình độc lập và 5 dữ liệu thử tƣơng ứng thỏa mãn tiêu chí bao phủ độc lập:
DT1 = {abcx} DT2 = {x} DT3 = {acx} DT4 = {ax} DT5= {abc}
2.3.2. Kiểm thử dựa trên đồ thị luồng dữ liệu
Đồ thị luồng dữ liệu là một sự mở rộng của đồ thị luồng điều khiển bằng cách bổ sung thêm xử lý dữ liệu trong chƣơng trình. Việc xử lý dữ liệu trong chƣơng trình đƣợc thực hiện thông qua sự định nghĩa và sử dụng các biến. Định nghĩa, đƣợc kí hiệu là def, là vị trí mà giá trị của biến đƣợc lƣu trữ trong bộ nhớ. Sử dụng, đƣợc kí hiệu là use, là vị trí mà biến đƣợc truy cập.
Lộ trình định nghĩa- sử dụng, đƣợc kí hiệu du-path.Tiêu chí bao phủ cho đồ thị luồng dữ liệu sẽ đƣợc định nghĩa dựa trên khái niệm du-path.Chúng ta chia du- path thành 2 nhóm:
- du-path liên quan đến định nghĩa biến, gồm các du-pathvới 1 biến đƣợc định nghĩa tại 1 đỉnh. du(ni,v) là tập các du-path đối với biến v xuất phát từ đỉnh ni.
- du-path liên quan đến các cặp định nghĩa và sử dụng biến, gồm các du-path
đối với 1 biến đƣợc định nghĩa tại một đỉnh và đƣợc sử dụng tại một đỉnh khác.du(ni, nj, v) là tập các du-path đối với biến v xuất phát từ đỉnh ni và kết thúc tại đỉnh nj.
Tiêu chí bao phủ định nghĩa
Tiêu chí bao phủ định nghĩa, đƣợc kí hiệu all-defs, yêu cầu mỗi định nghĩa biến đạt ít nhất một sự sử dụng. Nghĩa là, đối với mỗi tập các du-path thuộc du(n,v), ít nhất một du-path phải đƣợc bao phủ.
Ví dụ: chúng ta xem xét hàm foo dƣới đây:
int foo (int a, int b, int c) { int s=10, r; if (a> b) a=a-b; else a=b-a; if (a> c) r=s+a; else r=s+c; return r*r; }
Hình 2. 8 - Đồ thị luồng điều khiển biểu diễn hàm foo
Chúng ta áp dụng tiêu chí định nghĩa đối với biến s. Biến s đƣợc định nghĩa tại đinh n0, tập các du-path đối với biến s xuất phát từ đỉnh n0 là:
du(n0, s) = {[ n0 n1 n2 n4 n5 ], [ n0 n1 n3 n4 n5 ], [ n0 n1 n2 n4 n6 ], [ n0 n1 n3 n4 n6 ]}
Nhƣ vậy, để thỏa mãn tiêu chí bao phủ định nghĩa, chúng ta có thể bao phủ
du-path: [n0 n1 n2 n4 n5]. Có thể chọn kiểu dữ liệu thử DT= {a=5, b =2, c=1}.
𝑑𝑒𝑓 𝑛3 = {𝑎} 𝑑𝑒𝑓 𝑛6 = {𝑟} 𝑛0 𝑛1 𝑛2 𝑛3 𝑛4 𝑛5 𝑛6 𝑛7 𝑑𝑒𝑓 𝑛2 = {𝑎} 𝑓𝑜𝑜(𝑎, 𝑏, 𝑐) 𝑠 = 10 𝑎 > 𝑏 a ≤ 𝑏 𝑢𝑠𝑒(𝑛1, 𝑛2) = {a, b} 𝑢𝑠𝑒(𝑛1, 𝑛3) = {a, b} 𝑑𝑒𝑓 𝑛0 = {𝑎, 𝑏, 𝑐, 𝑠} 𝑢𝑠𝑒(𝑛2) = {a, b} 𝑢𝑠𝑒(𝑛3) = {a, b} 𝑎 = 𝑎 − 𝑏 𝑎 = 𝑏 − 𝑎 𝑎 > 𝑐 a ≤ 𝑐 𝑢𝑠𝑒(𝑛4, 𝑛6) = {a, c} 𝑢𝑠𝑒(𝑛4, 𝑛6) = {a, c} 𝑢𝑠𝑒(𝑛5) = {s, a} 𝑑𝑒𝑓 𝑛5 = {𝑟} 𝑢𝑠𝑒(𝑛6) = {s, c} 𝑟 = 𝑠 + 𝑎 𝑟 = 𝑠 + 𝑐 𝑢𝑠𝑒(𝑛7) = {r} 𝑟𝑒𝑡𝑢𝑟𝑛 𝑟 ∗ 𝑟
Tiêu chí bao phủ sử dụng
Trong ví dụ trên, chúng ta có thể nhận thấy tiêu chí bao phủ định nghĩa không đủ để phát hiện lỗi, nếu giả sử tại định n6 có lỗi. Tiêu chí bao phủ sử dụng sẽ giúp chúng ta khắc phục hạn chế này.
Tiêu chí bao phủsử dụng, đƣợc kí hiệu all-uses,yêu cầu mỗi định nghĩa biến đạt đến tất cả các sự sử dụng có thể. Nghĩa là, đối với mỗi tập các du-path thuộc du(ni, nj, v), ít nhất một du-path phải đƣợc bao phủ.
Áp dụng tiêu chí bao phủ sử dụng đối với biến s. Biến s đƣợc định nghĩa tại n0 và đƣợc sử dụng tại 2 đỉnh n5, n6. Vậy các du-path có thể là:
du(n0,n5,s)= {[ n0 n1 n2 n4 n5 ], [ n0 n1 n3 n4 n5 ]} du(n0,n6,s)= {[ n0 n1 n2 n4 n6 ], [ n0 n1 n3 n4 n6 ]} Chúng ta có thể chọn 2 dữ liệu:
DT1= {a=5, b =2, c=1}. DT2= {a=5, b =2, c=5}.
Tiêu chí bao phủ định nghĩa - sử dụng
Tiêu chí bào phủ định nghĩa - sử dụng, đƣợc kí hiệu all-du-paths, yêu cầu mỗi định nghĩa biến đạt đến đƣợc tất cả các sự sử dụng có thể thông qua tất cả các
du-path có thể. Nghĩa là, đối với mỗi tập các du-path thuộc du(ni, nj, v), tất cả các
du-path phải đƣợc bao phủ.
Quay trở lại ví dụ hàm foo, áp dụng tiêu chí all-du-paths đối với biến s, cần bao phủ 4 du-path: [ n0 n1 n2 n4 n5 ], [ n0 n1 n3 n4 n5 ], [n0 n1 n2 n4 n6 ], [ n0 n1 n3 n4 n6 ] Có thể chọn các dữ liệu thử tƣơng ứng: DT1= {a=5, b =2, c=1}. DT2= {a=2, b =5, c=1}. DT3= {a=5, b =2, c=5}. DT4= {a=2, b =5, c=5}.
2.3.3. Kiểm thử điều kiện
Kiểm thử điều kiện là phƣơng pháp thiết kế trƣờng hợp kiểm thử thực thi các điều kiện logic trong module chƣơng trình.
Điều kiện đơn: là một điều kiện Boolean (True/False) hay điều kiện quan hệ (<, >, <=, >=), có thể có toán tử NOT (!) đứng trƣớc.
Biểu thức quan hệ: là một biểu thức dƣới dạng E1 <op> E2, trong đó E1, E2 là các biểu thức số học và <op> là toán tử quan hệ có thể là một trong các dạng sau:<, >, <=, >=, ==, !=. Ví dụ, a > b +3.
Điều kiện phức: gồm các điều kiện đơn nối kết bằng các toán tử quan hệ (&&, ||, !,…). Ví dụ: (a> b+ 1) && (a<max).
Các chiến lƣợc kiểm thử điều kiện bao gồm:
Kiểm thử nhánh: phân rã các điều kiện tổng hợp thành các điều kiện đơn giản và thực hiện kiểm thử trên các nhánh đơn giản này ít nhất 1 lần.
Kiểm thử miền: kiểm thử dựa vào điều kiện quan hệ và toán tử quan hệ. Ví dụ, với biểu thức quan hệ có dạng E1<op> E2, cần có 3 kiểm thử đƣợc thiết kế cho E1==E2, E1>E2, E1<E2.
Kiểm thử nhánh và toán tử quan hệ: thực hiện trên các điều kiện tổng hợp. Tất cả các tổ hợp có thể có từ các điều kiện đơn giản đều đƣợc kiểm thử.
2.4. SO SÁNH KIỂM THỬ HỘP ĐEN VÀ KIỂM THỬ HỘP TRẮNG
Kiểm thử hộp đen(BBT) Kiểm thử hộp trắng(WBT)
Ƣu điểm
- Tester không cần am hiểu mã lệnh hay ngôn ngữ lập trình. - Tester thực hiện quan điểm của ngƣời sử dụng.
- Giúp phát hiện lỗi trongđặc tả chức năng.
-Kiểm tra đƣợc toàn bộ chƣơng trình nguồn.
- Phát hiện lỗi tại chỗ. - Tự động hóa kiểm thử.
Nhƣợc điểm
- Chỉ có một số nhỏ đầu vào thực sự đƣợc thực hiện. - Có thể để lại nhiều chƣơng trình không đƣợc kiểm tra.
- Yêu cầu ngƣời kiểm thử phải am hiểu mã lệnh chƣơng trình. Do đó đòi hỏi tài nguyên, nhân lực và máy tốn kém.
- Có khả năng tồn tại các tổ hợp lệnh khác nhau gây lỗi.
CHƢƠNG 3
MỘT SỐ ỨNG DỤNGCỦA QUY TRÌNH KIỂM THỬ
Chƣơng này tìm hiểu một số bài toán cụ thể và nghiên cứu xây dựng các bộ dữ liệu kiểm thử cho bài toán cùng các chƣơng trình kiểm thử tự động.
Theo các quan niệm hiện đại, kiểm thử phần mềm cần đƣợc tiến hành ngay từ các pha đầu tiên của qui trình phát triển phần mềm và xuyên suốt cho đến tận pha cuối cùng của qui trình này [9],[16], Nhƣ vậy, kiểm thử phần mềm không chỉ là xác định các tập dữ liệu, gọi là dữ liệu test để thực hiện phần mềm trên các tập dữ liệu đó. Ngƣời ta phân biệt hai loại test chính là logic test và run test.
Logic test là những tình huông giả định ở pha thiết kế, lúc chƣa có chƣơng trình. Logic test giúp cho ngƣời thiết kế phát hiện đƣợc những điểm mâu thuẫn, bất hợp lí trong thiết kế. Logic test đƣợc thực hiện chủ yếu trên các sơ đồ.
Run test đƣợc tiến hành khi cho chƣơng trình thực hiện trên những bộ dữ liệu cụ thể.
Ta xét một thí dụ liên quan đến module chọn chỗ ngồi trên chuyến bay trong hệ thống checkin online chẳng hạn.
Khi thảo luận về bản thiết kế, ngƣời ta có thể đặt ra những tình huống sau đây: