Trong phần 1 chúng ta đã tìm hiểu về TDD, biết được mô hình hoạt động của TDD. Phần tiếp theo này chúng ta sẽ tìm hiểu một ví dụ minh hoạ về TDD.
Việc phát triển - dựa theo hướng kiểm thử yêu cầu
các thử nghiệm xuất hiện trước. Chỉ sau khi bạn đã viết các thử nghiệm (và thất
bại) bạn mới viết mã lệnh được thử nghiệm. Nhiều nhà phát triển sử dụng một biến
thể cách làm thử nghiệm được gọi là phát triển thử nghiệm sau (TAD), ở đó bạn
viết mã lệnh và sau đó viết các thử nghiệm đơn vị. Trong trường hợp này, bạn vẫn
nhận được các thử nghiệm, nhưng bạn không nhận được các khía cạnh thiết kế nổi
dần của TDD. Chẳng có gì ngăn cản bạn viết mã lệnh cực kỳ ghớm guốc và sau đó
lúng túng tìm cách để thử nghiệm nó như thế nào.
Khi viết mã lệnh trước, bạn đã
nhúng các định kiến của bạn về cách thức mã sẽ hoạt động ra sao, sau đó thử
nghiệm nó. TDD đòi hỏi bạn phải làm ngược lại: viết các thử nghiệm trước và cho
phép nó thông báo cho bạn cách làm thế nào để viết mã lệnh làm cho thử nghiệm
thông qua. Để minh họa sự khác biệt quan trọng này, chúng sẽ bắt đầu một ví dụ.
Ví dụ minh họa về TDD
Để cho thấy các lợi ích thiết kế của TDD, cần một bài toán để
giải quyết. Dưới đây là một minh họa về TDD khi thực hiện chương trình tính tổng
của hai số nguyên cho trước, mã lệnh viết bằng java, đơn vị kiểm thử
dùng JUnit.
Sprint 1: Tạo kiểm thử và
làm cho nó thất bại
Phiên bản đầu tiên
của Calculator dành cho việc tính tổng của hai số nguyên, hàm ném ra lỗi khi được
gọi đến
package cal;
public class
Calculator {
public int add(int x, int y) {
throw new
UnsupportedOperationException("not support operator");
}
}
|
Bây giờ cần tạo ra
kiểm thử thứ nhất: hàm testAdd() kiểm tra mã lệnh trên có thực thi không?
public class
CalculatorTest {
@Test
public void testAdd() {
int x = 1;
int y = 1;
Calculator instance = new
Calculator();
int expResult = 2;
int result = instance.add(x, y);
assertEquals(expResult, result);
}
}
|
Với x, y cùng nhận giá trị 1, và kết quả mong đợi expResult là
2. Khi gọi hàm add(x, y) thì luôn trả về thông báo lỗi, do hàm add chưa hỗ trợ
thao tác tính toán trong đó.
Sprint 2: Quay lại phiên bản
đầu tiên của Calculator để sửa lại mã lệnh theo cách đơn giản nhất có thể, làm
cho kiểm thử vượt qua.
Phiên bản thứ hai
của Calculator
package cal;
public class Calculator {
public int add(int x, int y) {
return x + y;
}
}
|
Sau khi sửa xong
mã lệnh để giúp kiểm thử không lỗi, ta chạy lại kiểm thử đầu tiên testAadd thì
thấy nó đã vượt qua.
Sprint 3: Tạo tiếp kiểm thử
thứ hai kiểm tra xem nếu cộng một số với một số có giá trị bằng giá trị lớn nhất
theo kiểu dữ liệu lưu trữ.
…
@Test
public void
testAdd2() {
int x =
Integer.MAX_VALUE;
int y = 1;
Calculator
instance = new Calculator();
try {
int
result = instance.add(x, y);
assertFalse(true);
} catch
(Exception e) {
assertTrue(true);
}
}
…
|
Khi chạy kiểm thử
này ta thấy nó bị thất bại vì không thể cộng thêm bất kỳ giá trị nào nữa cho x
khi mặc định nó đã nhận giá trị lớn nhất. Dòng lệnh int result =
instance.add(x, y) đặt trong khối lệnh try… catch sẽ ném ra lỗi nếu như có bất
cứ thông báo nào lỗi nào xảy ra assertFalse(true) và bắt lỗi nếu không có thông
báo lỗi được gửi đến assertTrue(true). Và khi ta chạy chương trình thì đúng là
có thông báo lỗi được đưa ra.
Sprint 4: Để kiểm thử vượt
qua được ta lại quay lại phiên bản thứ hai của Calculator và thiết kế, cấu trúc
lại cho đến khi kiểm thử vượt qua.
Phiên bản thứ ba của
Calculator
package cal;
public class Calculator {
public int
add(int x, int y) {
if (x
/ 2 + y / 2 >= Integer.MAX_VALUE / 2) {
throw new RuntimeException("out of range exception");
}
return
x + y;
}
}
|
Nếu tổng của hai số
x, y vượt quá khoảng giới hạn thì lỗi “out of range exception” sẽ được ném ra.
Sprint 5: Quay lại bản kiểm
thử thứ hai testAdd2() và thực thi kiểm thử này thấy nó đã được vượt qua
Sprint 6: Tương tự như thế
đối với trường hợp nếu cộng giá trị nhỏ nhất của một số với một số âm. Ta tiếp
tục thiết kế kiểm thử.
Ta có bản kiểm thử
thứ 3
…
@Test
public void
testAdd3() {
int x =
Integer.MIN_VALUE;
int y = -1;
Calculator
instance = new Calculator();
try {
int
result = instance.add(x, y);
assertFalse(true);
} catch
(Exception e) {
assertTrue(true);
}
}
…
|
Bản kiểm thử này
cũng đưa ra lỗi và cần quay lại phiên bản thứ 3 của Calculator để sửa cho đến
khi hết lỗi.
Ta có phiên bản thứ
tư của Calculator
package cal;
public class Calculator {
public int
add(int x, int y) {
if (x
/ 2 + y / 2 >= Integer.MAX_VALUE / 2) {
throw new RuntimeException("out of range exception");
}
if (x
/ 2 + y / 2 <= Integer.MIN_VALUE / 2) {
throw new RuntimeException("out of range exception");
}
return
x + y;
}
}
|
Sprint 7: Như vậy ta đã tạo
được ít nhất ba bản kiểm thử đơn vị cho một hàm rất đơn giản là cộng hai số
nguyên. Bạn có thể suy nghĩ tiếp và tạo thêm các bản kiểm thử khác và kiểm lỗi
rồi quay lại Calculator để thiết kế và tái cấu trúc lại sao cho tất cả các bản
kiểm thử này được vượt qua.
Sprint 8: Giả sử ta đã vượt
qua tất cả các bản kiểm thử, nhưng chúng ta lại nhìn thấy tên biến x, y trong
hàm add không được rõ nghĩa. Ta có thể cấu trúc lại hàm add để nó phù hợp hơn
như sau:
package cal;
public class Calculator {
public int
add(int firstOperand, int secondOperand) {
if
(firstOperand / 2 + secondOperand / 2 >= Integer.MAX_VALUE / 2) {
throw new RuntimeException("out of range exception");
}
if
(firstOperand / 2 + secondOperand / 2 <= Integer.MIN_VALUE / 2) {
throw new RuntimeException("out of range exception");
}
return
firstOperand + secondOperand;
}
}
|
Vậy qua các bản kiểm
thử và các lần tái cấu trúc ta đã có được phiên bản đầy đủ và tốt của
Calculator.
Vấn đề đặt ra là
có những chiến lược nào để giúp ta thiết kế kiểm thử và tái cấu trúc mã nguồn?
Chiến lược tạo kiểm thử
Một khung kiểm thử tốt sẽ giúp bạn tránh được việc viết quá
nhiều mã dư thừa. Đã có rất nhiều các phương pháp viết kiểm thử, trong bài này
nói về hai phương pháp kiểm thử đơn vị và kiếm thử chấp nhận tự động.
Bạn có thể đọc thêm trong cuốn “The Art of Unit Testing” của
Roy Overshove về kiểm thử đơn vị.
Trong kiểm thử tự động, thành phần cơ bản nhỏ nhất là “phương
thức dưới kiểm thử” (MUT). Lý tưởng là mỗi một kiểm thử chỉ xác nhận một khía cạnh
của một hàm trong một lớp. Nếu kiểm thử được đặt tên hợp lý, bạn sẽ biết ngay
kiểm thử nào đang có vấn đề. Hãy thử theo một con đường lô-gíc xuyên suốt mã
nguồn của bạn, càng chi tiết thì càng có ý nghĩa thiết thực. Khi bạn đã có đủ
các kiểm thử, bằng cách chạy chúng, bạn có thể chứng minh rằng mọi phương thức
đều hoạt động đúng như mong muốn.
Khi viết các kiểm thử đơn vị, bạn nên:
1. Bắt đầu với “trường hợp chính” hay: các kiểm thử của một
chức năng đã định.
2. “trường hợp biên”.
Hình 4: Minh họa tạo kiểm thử với trường hợp biên
3. “trường hợp có mùi” – hay: báo cáo lỗi (bugs)
Hình 5: Minh họa tạo kiểm thử với trường hợp biên
Thường thì việc tạo ra các trường hợp tốt là đủ với kiểm thử
đơn vị, vì các trường hợp khác có thể được đưa vào một cách dễ dàng khi cần thiết
– với điều kiện cấu trúc chương trình của bạn có đủ độ linh hoạt.
Với việc tạo ra các kiểm thử đơn vị tự động, bạn có thể chắc
rằng:
- Các chức năng của phương thức không ngẫu
nhiên bị thay đổi
- Lớp sẽ tiếp tục hoạt động như bạn mong đợi
nếu nó vượt qua các kiểm thử sau khi sắp xếp lại mã nguồn
- Sự tương tác giữa các lớp là rõ ràng
Các kiểm thử đơn vị sẽ giúp bạn tìm ra vấn đề trong mã nguồn
của mình từ rất sớm, trước cả khi bạn đưa nó cho một người khác xem xét. Bạn sẽ
không cần sử dụng phần mềm tìm lỗi (debugger). Kiểm thử còn là một hợp đồng phần
mềm vì nó sẽ thông báo ngay lập tức với bạn khi mã ngừng hoạt động như đã đặc tả.
Ở một mức độ nào đó, nó giúp ích cho việc thiết kế. Nó cụ thể hóa giải pháp mà
không cần phải thực thi các chi tiết. Sẽ dễ dàng hơn cho bạn khi tập trung vào
cách đơn giản nhất có thể để giải quyết yêu cầu.
Phần tiếp theo chúng ta sẽ tìm hiểu về kiểm thử chấp nhận tự động.
Đăng nhận xét