Вы находитесь на странице: 1из 10

KIỂM THỬ CHƯƠNG TRÌNH VỚI JUNIT

TESTING WITH JUNIT


Nguyễn Công Vũ, Phạm Đại Xuân
Khoa Công nghệ thông tin, Đại học Nông Lâm TP.HCM
Email: ncv@hcmuaf.edu.vn hoặc ncv@hcm.fpt.vn

SUMMARY

In software development, maintaining and testing are considered as boring tasks when
comparing to programming. Software people are more excited to new and innovative
things than to those that are already well known to them. However, the more software
are produced, the more bugs we observe. Testing is an indispensable activity in
software development for bug detection and quality assurance. This writing
introduces the use an open source tool known as JUnit to facilitate the unit testing
activity. Recognizing the importance of this tool, many integrated development
environments (IDEs), such as JBuilder, have integrated JUnit to their architecture.

ĐẶT VẤN ĐỀ

Trong chuyến công tác tại Đại học Calgary – Canada vào cuối năm 2003, chúng tôi
nhận thấy nhiều môn học liên quan đến công nghệ phần mềm, ở bậc đại học lẫn cao
học, đã nhấn mạnh đến công đoạn kiểm thử chương trình. Mọi chương trình đều phải
đi kèm với bộ số liệu kiểm thử. Tuy nhiên, hoạt động này chưa thực sự phổ biến trong
chương trình giảng dạy lập trình và công nghệ phần mềm tại một số trường đại học ở
nước ta. Người lập trình thường xem nhẹ việc kiểm thử, đơn giản vì đó là một công
việc nhàm chán, ít gây hứng thú. Nhưng kiểm thử là một hoạt động quan trọng và
không thể thiếu được nhằm phát hiện lỗi trong chương trình, từ đó nâng cao năng suất
và đảm bảo chất lượng sản phẩm phần mềm. Beck và Gamma là những người đầu tiên
phát triển công cụ mã nguồn mở JUnit để hỗ trợ việc kiểm thử. Bài viết này sẽ trình
bày lại một ví dụ minh họa việc áp dụng JUnit bằng việc đưa ra một thiết kế đơn giản
và hợp lý để giải quyết bài toán đặt ra.

LỢI ÍCH CỦA JUNIT

JUnit tránh cho người lập trình phải làm đi làm lại những việc kiểm thử nhàm chán
bằng cách tách biệt mã kiểm thử ra khỏi mã chương trình, đồng thời tự động hóa việc
tổ chức và thi hành các bộ số liệu kiểm thử.

Thoạt tiên, khi sử dụng JUnit, ta có thể có cảm giác là JUnit chỉ làm mất thêm thời
gian cho việc kiểm thử: Thay vì phải viết thêm các lớp và phương thức mới phục vụ
cho công tác kiểm thử, ta có thể soạn nhanh một bộ số liệu rồi viết ngay vào trong
phương thức main() và quan sát ngay kết quả kiểm thử. Vì quá trình soạn số liệu và
quá trình kiểm thử diễn ra đồng thời, nên ta sẽ dễ dàng nhận biết được ngay chương
trình đã chạy đúng trên bộ số liệu kiểm thử hay không, mà không cần nhìn vào tín
hiệu “xanh” mà JUnit có thể hỗ trợ.

Nhưng khi tổ chức lại chương trình cho hợp lý hơn (refactoring) hoặc khi phải thay
đổi chương trình để phục vụ cho nhu cầu mới, các bộ số liệu kiểm thử trước đây sẽ
cần được sử dụng lại để chắc chắn rằng những thay đổi trong chương trình không làm
phương hại đến những thành quả trước đó, lúc này ta sẽ phải mất thời gian để tìm hiểu
lại xem bộ số liệu trước đây sẽ tương ứng với kết xuất gì vì ta không thể nhớ hết mọi
hoạt động kiểm thử đã diễn ra. Việc nhớ lại những kiểm thử đã qua sẽ chẳng thú vị vì
không đem đến cho ta điều gì mới. Nếu phải kiểm thử trên những bộ số liệu lớn thì
gánh nặng của việc tổ chức kiểm thử sẽ chồng chất thêm. JUnit giúp người lập trình
tự động hóa các công việc nhàm chán, và chỉ cần nhìn thấy tín hiệu “xanh” là ta có thể
yên tâm. Phụ lục A sẽ hướng dẫn việc tích hợp JUnit vào môi trường Eclipse.

VÍ DỤ MINH HỌA

Sau đây là một ví dụ minh họa với những yêu cầu mới dần dần được thêm vào: Hãy
thiết kế lớp tiền tệ. Tiền tệ được đặc trưng bằng số tiền và đơn vị tiền (chẳng hạn
VND hoặc USD). Trước yêu cầu này, ta dễ dàng viết ra lớp Money ở Hình 1.

public class Money {


private double amount;
private String currency;

public Money(double amount, String currency) {


this.amount = amount;
this.currency = currency;
}
}
Hình 1. Lớp Money dùng để biểu diễn một loại tiền tệ

Bây giờ giả sử rằng ta chỉ cần xử lý một loại tiền tệ duy nhất, chẳng hạn tiền Việt
Nam. Hãy hiện thực phương thức dưới đây để cộng hai số tiền cùng loại với nhau, và
dùng JUnit để kiểm thử chương trình.
Money add(Money money)

Hình 2 sẽ trình bày phần hiệu chỉnh lớp Money nhằm giải quyết yêu cầu mới vừa đặt
ra.

public class Money {


// Phần tương tự như trong Hình 1 đã được lược bớt

public Money add(Money money) {


return new Money(this.amount + money.amount, this.currency);
}
}

import junit.framework.Assert;
import junit.framework.TestCase;

public class MoneyTest extends TestCase {


public void testAdd() {
Money m1 = new Money(200, "VND");
Money m2 = new Money(1000, "VND");
Money result = m1.add(m2); // đối tượng lưu kết quả tính toán
Money expected = new Money(1200, "VND"); // kết quả dự kiến
Assert.assertTrue(result.equals(expected)); // lệnh kiểm thử
}
}
Hình 2. Phương thức add() được bổ sung vào lớp Money
Phần kiểm thử có bốn điểm đáng chú ý sau:
1. Phương thức kiểm thử cần được đặt tên bắt đầu bằng từ test
2. Đối tượng lưu kết quả tính toán,
3. Đối tượng lưu kết quả dự kiến, và
4. Kiểm chứng sự trùng khớp giữa kết quả tính toán và kết quả dự kiến.

Ở đây ta đã dùng lệnh assertTrue() và phương thức equals() vì kết quả và dự kiến là
hai đối tượng. Nếu kết quả và dự kiến thuộc kiểu dữ liệu nguyên thủy, chẳng hạn int,
thì ta có thể dùng một trong hai lệnh sau:
Assert.assertTrue(result == expected);
Assert.assertEquals(result, expected);

Lệnh kiểm chứng thứ hai sẽ cung cấp nhiều thông tin hơn nếu kết quả tính toán và dự
kiến không trùng khớp nhau, từ đó giúp người lập trình nhanh chóng phát hiện lý do
gây ra lỗi bên trong chương trình. (Thi hành việc kiểm thử JUnit trong môi trường
Eclipse: Chọn lớp kiểm thử → Kích vào menu Run → Run As → JUnit Test. Có thể
cần click vào thẻ JUnit ở phía dưới bên trái màn hình để thấy được tín hiệu “xanh”
hoặc “đỏ”)

Sau khi thi hành JUnit Test, ta sẽ gặp tín hiệu “đỏ” (Hình 3), nghĩa là chương trình đã
có lỗi. Để có được tín hiệu “xanh”, ta cần định nghĩa lại (override) phương thức
equals() bên trong lớp Money để so sánh bằng giữa hai đối tượng (Hình 4).

Hình 3. Kết quả kiểm thử khi thi hành lớp MoneyTest ở Hình 2

public class Money {


// Phần tương tự như trong Hình 2 đã được lược bớt

public boolean equals(Object object) {


if (object instanceof Money) {
Money money = (Money)object;
return this.amount == money.amount &&
this.currency == money.currency;
}
return false;
}
}
Hình 4. Định nghĩa lại phương thức equals() trong lớp Money

Ta cũng cần viết mã để kiểm thử tính đúng đắn của phương thức equals() (Hình 5).
Sau khi thi hành JUnit Test, ta sẽ được tín hiệu “xanh” (Hình 6). Điều cần lưu ý là
phương thức equals() ở Hình 4 không thật sự an toàn nếu số tiền có phần thập phân,
nhưng chi tiết tế nhị này được cố ý bỏ qua để đơn giản hóa vấn đề đang đề cập.

public class MoneyTest extends TestCase {


public void testAdd() {
Money m1 = new Money(200, "VND");
Money m2 = new Money(1000, "VND");
Money result = m1.add(m2);
Money expected = new Money(1200, "VND");
Assert.assertTrue(result.equals(expected));
}

public void testEquals() {


Money m1 = new Money(200, "VND");
Money m2 = new Money(1000, "VND");
Assert.assertTrue(m1.equals(new Money(200, "VND")));
Assert.assertFalse(m1.equals(m2));
}
}
Hình 5. Mã kiểm thử sau khi định nghĩa lại phương thức equals()

Hình 6. Kết quả kiểm thử sau khi định nghĩa lại phương thức equals()

Quan sát mã kiểm thử ở Hình 5, ta thấy cần phải tổ chức lại vì đã có sự trùng lặp mã.
Để loại bỏ sự trùng lặp mã này, ta có thể chuyển hai đối tượng cục bộ m1 và m2 thành
hai thuộc tính riêng tư. (Chuyển biến cục bộ thành thuộc tính lớp trong môi trường
Eclipse: Kích vào biến cục bộ cần chuyển → menu Refactor → Convert Local
Variable to Field… → Initialize in: Field declaration)
Bây giờ giả sử rằng ta lại có thêm một yêu cầu mới: Các đơn vị kinh doanh có thể giữ
nhiều hơn một loại tiền, chẳng hạn vừa có VND vừa có USD. Khi thêm một loại tiền
khác với loại tiền hiện có thì chương trình không được cộng vào số tiền hiện có mà
phải lưu trữ riêng loại tiền mới này. Ví dụ, nếu hiện thời ta có 1200 VND, thì sau khi
thêm 10 USD, ta sẽ có 1200 VND và 10 USD. Hãy thiết kế chương trình để thực hiện
yêu cầu trên.

Để hoàn thành yêu cầu, ta sẽ thiết kế thêm một lớp mới có tên là MoneyBag để lưu trữ
một danh sách các loại tiền khác nhau, và viết thêm một lớp kiểm thử mới (Hình 7).

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class MoneyBag {


private List monies = new ArrayList();

public String toString() {


String s = "{ ";
Iterator iter = monies.iterator();
while (iter.hasNext())
s += iter.next() + " ";
return s + "}";
}

public void add(Money money) {


for (int i = 0; i < monies.size(); i++) {
Money m = (Money)monies.get(i);
if (money.getCurrency().equals(m.getCurrency())) {
monies.set(i, m.add(money));
return;
}
}
// money is a new currency
monies.add(money);
}
}

import junit.framework.Assert;
import junit.framework.TestCase;

public class MoneyBagTest extends TestCase {


public void testAddWithToString() {
MoneyBag bag = new MoneyBag();
Assert.assertEquals(bag.toString(), "{ }");

bag.add(new Money(1200, "VND"));


Assert.assertEquals(bag.toString(), "{ [1200 VND] }");

bag.add(new Money(10, "USD"));


Assert.assertEquals(bag.toString(), "{ [1200 VND] [10 USD] }");
}
}
Hình 7. Lớp MoneyBag và mã kiểm thử
Khi thi hành JUnit Test, ta sẽ gặp tín hiệu “đỏ”. Bằng việc chèn lệnh
System.out.println() vào phương thức testAddWithToString(), ta sẽ phát hiện ra kết
quả phải là { [1200.0 VND] }, thay vì { [1200 VND] }. Từ đó ta biết rằng cần phải hiệu
chỉnh kết quả để được tín hiệu “xanh”. Điều này một lần nữa cho thấy điểm tế nhị khi
phải thực hiện việc so sánh trên các số thực.

Sau đây là câu hỏi dành cho bạn đọc trước khi chúng tôi trình bày tiếp: Tại sao không
viết phương thức equals() trong lớp MoneyBag và sử dụng nó trong JUnit Test?

Ngoài việc dùng phương thức toString() để kiểm thử, ta còn có thể viết phương thức
equals() và một phương thức bổ trợ, được đặt tên là contains(), để kiểm tra xem đối
tượng MoneyBag có chứa một đối tượng Money cho trước không (Hình 8).

Vì MoneyBag chỉ chứa các đối tượng Money phân biệt, nên khi xét tính bằng nhau
giữa hai đối tượng MoneyBag trong phương thức equals(), ta chỉ cần kiểm chứng đối
tượng này có phải là tập con của đối tượng kia và số phần tử của hai đối tượng cần
bằng nhau là đủ. Việc kiểm thử tính đúng đắn của các phương thức equals() được thể
hiện trong phương thức testEquals() (Hình 9). Ngoài ra, vì thuộc tính của phương
thức bổ trợ contains() là private nên ta không thể kiểm thử riêng phương thức này
trong lớp MoneyBagTest, mà đã chuyển vào phương thức main() của lớp MoneyBag.

public class MoneyBag {


// Phần tương tự như trong Hình 7 đã được lược bớt

private boolean contains(Money money) {


Iterator iter = monies.iterator();
while (iter.hasNext()) {
if (money.equals((Money)iter.next()))
return true;
}
return false;
}

public boolean equals(Object object) {


if (object instanceof MoneyBag) {
MoneyBag bag = (MoneyBag)object;
if (monies.size() != bag.monies.size())
return false;
Iterator iter = bag.monies.iterator();
while (iter.hasNext()) {
Money m = (Money)iter.next();
if (!monies.contains(m))
return false;
}
return true;
}
return false;
}

public static void main(String[] args) {


MoneyBag bag = new MoneyBag();
Money m1 = new Money(1200, "VND");
Money m2 = new Money(10, "USD");

System.out.println(!bag.contains(m1));

bag.add(m1);
System.out.println(bag.contains(m1));
System.out.println(!bag.contains(m2));

bag.add(m2);
System.out.println(bag.contains(m2));
}
}
Hình 8. Bổ sung vào MoneyBag hai phương thức contains() và equals()

public class MoneyBagTest extends TestCase {


public void testAddWithToString() {
// Phần tương tự như trong Hình 7 đã được lược bớt
}

public void testAddWithEquals() {


MoneyBag bag1 = new MoneyBag();
MoneyBag bag2 = new MoneyBag();
Assert.assertTrue(bag1.equals(bag2));

bag1.add(new Money(1200, "VND"));


Assert.assertFalse(bag1.equals(bag2));

bag1.add(new Money(10, "USD"));

bag2.add(new Money(10, "USD"));


bag2.add(new Money(1200, "VND"));
Assert.assertTrue(bag1.equals(bag2));
}
}
Hình 9. Bổ sung phương thức testAddWithEquals() vào lớp kiểm thử

Để kết hợp đồng thời nhiều lớp lớp kiểm thử khác nhau, ta có thể viết ra một bộ kiểm
thử (TestSuit) như được minh họa ở Hình 10. (Trong môi trường Eclipse: Kích vào
menu File → New → Other… → Java → JUnit → TestSuit → Next → Finish)

import junit.framework.Test;
import junit.framework.TestSuite;

public class AllTests {


public static Test suite() {
TestSuite suite = new TestSuite(“Test for Money”);
suite.addTest(new TestSuite(MoneyTest.class));
suite.addTest(new TestSuite(MoneyBagTest.class));
return suite;
}
}
Hình 10. Kiểm thử nhiều bộ số liệu đồng thời

Sau đây là vài bài tập dành cho bạn đọc:


• Hãy đối chiếu hai cách kiểm thử dùng toString() và equals() rồi cho nhận xét. Bạn
có cách kiểm thử nào khác không?

• Hãy bổ sung phương thức subtract() vào lớp MoneyBag để trừ bớt một số tiền cho
trước từ số tiền hiện có. Đừng quên kiểm thử với JUnit.

• Như đã được đề cập, bài viết này chủ yếu dựa trên bài viết của Beck và Gamma.
Hãy đọc bài đó và đối chiếu, rồi cho nhận xét về phương án thiết kế lời giải của
hai bài viết.

KẾT LUẬN

Các thao tác sửa mã chương trình trong quá trình bảo trì phần mềm là nỗi lo thường
trực đối với người phát triển chương trình vì đôi khi chỉ một thay đổi nhỏ cũng có thể
làm toàn bộ chương trình ngưng hoạt động. Công cụ JUnit giúp giảm bớt gánh nặng
này bằng cách đưa ra một khuôn khổ hợp lý nhằm tổ chức các bộ số liệu kiểm thử và
tự động hóa một số công việc nhàm chán, từ đó khuyến khích người lập trình thực
hiện việc kiểm thử thường xuyên hơn. Kiểm thử càng nhiều thì càng giúp nhanh
chóng phát hiện lỗi, dẫn đến việc giảm bớt lỗi trong chương trình, từ đó nâng cao
năng suất lập trình và chất lượng phần mềm. JUnit có thể kết hợp với những hoạt
động khác để tăng tính hiệu quả trong việc bắt lỗi. Một trong những hoạt động được
trường phái Lập trình Cực đoan (Extreme Programming) khuyến khích đó là lập trình
bắt cặp (pair programming) và chúng tôi đang thực hiện một vài thực nghiệm về hoạt
động lý thú này.

CẢM TẠ

Chúng tôi chân thành cảm ơn Viện Công nghệ thông tin và Công nghệ phần mềm –
Đại học Liên hiệp quốc (UNU/IIST), Macao và Khoa Kỹ nghệ Điện và Máy tính –
Đại học Calgary, Canada đã tạo điều kiện cho chúng tôi tiếp cận với phương pháp
kiểm thử dùng JUnit.

TÀI LIỆU THAM KHẢO

BECK, K., GAMMA. E, 1998. Test Infected: Programmers Love Writing Tests. Java
Report, Vol. 3, No. 7, p.51 – 56.
http://JUnit.sourceforge.net/doc/testinfected/testing.htm

Eclipse Tutorials. http://www.3plus4software.com/eclipse/index_en.html


PHỤ LỤC: TÍCH HỢP JUNIT VÀO MÔI TRƯỜNG ECLIPSE

1. Tải phần mềm JUnit từ website www.junit.org và trích ra file src.jar

2. Trong môi trường Eclipse tạo một project mới có tên junit

3. Chuyển file src.jar vào project junit: Kích vào menu File → Import… → Zip file →
Next (xem Hình A1) → Browse… để chuyển đến vị trí file src.jar rồi chọn lựa các
mục như được minh họa ở Hình A2 → Finish

Hình A1

Hình A2

4. Có thể thêm phần hỗ trợ Javadoc: Kích phải vào project junit → Properties →
Javadoc Location → chỉ đường dẫn về folder junit (xem ví dụ minh họa ở Hình
A3)
Hình A3

Các bước từ 1 đến 4 chỉ cần được thực hiện một lần duy nhất. Sau này mỗi khi tạo ra
một project chương trình mới và muốn kiểm thử dùng JUnit, ta cần thông báo cho
project này đường dẫn đến junit như sau:
• Click phải vào project cần áp dụng JUnit → Properties → Java Build Path →
click thẻ Project rồi chọn junit (xem minh họa ở Hình A4) → OK

Hình A4

Вам также может понравиться