Diễn Đàn Tin Học » Tutorial Room » Lập trình » Software Engineering » Craftsman-Truyện dài nhiều tập Craftsman 11 - Forget the Main() Chúng tôi đã dựng xong chương trình để gọi phần biên dịch SMC từ xa, gởi mã nguồn đến server và gởi ngược lại hồ sơ đã biên dịch. Thế nhưng tại sao Jerry lại khăng khăng test mã nguồn lẻ tẻ ?
Quên đi hàm main()
Chúng tôi đã dựng xong chương trình để gọi phần biên dịch SMC từ xa, gởi mã
nguồn đến server và gởi ngược lại hồ sơ đã biên dịch. Thế nhưng tại sao Jerry
lại khăng khăng test mã nguồn lẻ tẻ?
Phần 11.
Robert C. Martin
Trong đầu tôi cứ cân nhắc mãi mớ threads treo tòng teng trong khi ăn món mì ống
spaghetty một cách lơ đãng. Sau bữa trưa, tôi trở về phòng làm việc tìm Jerry.
"Ông C nghĩ là SocketServer sẵn sàng để dùng rồi đó,
và bây giờ ông ta muốn chúng mình làm việc với ứng dụng SMSRemote."
"Ồ, đúng nhỉ!" Tôi nói. "Thì đó là lý do có SocketServer mà
- mình đã dựng xong chương trình dùng để gọi phần biên dịch SMC từ xa, gởi mã
nguồn đến server và gởi ngược lại hồ sơ đã biên dịch."
Jerry nhìn tôi chờ đợi và hỏi, "mày nghĩ mình khởi công sao đây?"
"Tôi nghĩ là tôi cần biết người dùng sẽ sử dụng chúng ra sao cái đã," tôi trả
lời.
"Xuất sắc!" gã mỉm cười. "Khởi đầu từ cái nhìn của người dùng luôn luôn là một
điều hay. Thế thì cách nào là cách đơn giản nhất người dùng có thể mó đến tiện
ích này?"
"Anh ta có thể yêu cầu một hồ sơ nào đó được biên dịch," tôi trả lời. "Lệnh ấy
có thể như thế này." tôi viết lên tường như sau: java
SMCRemoteClient myFile.sm
"Coi được đó," Jerry nói. "Mình bắt đầu sao đây?"
Tôi cảm thấy khá vững tin sau khi làm SocketServer chạy
được, thế nên tôi vớ lấy bàn phím và bắt đầu gõ:
|
|
public class SMCRemoteClient {
public static void main(String
args[]) {
String fileName = args[0]; }
} |
|
"Mày có cái test cho nó không?" Jerry ngắt ngang.
"Ý ông là sao?" tôi hỏi một cách thiếu kiên nhẫn. "Mã nguồn này thuộc dạng lẻ tẻ
- sao mình phải viết cái test cho nó làm chi?"
"Nếu mày không viết một cái test cho nó thì làm sao mày biết là có cần hay
không?" gã hỏi.
Câu hỏi ấy làm tôi khựng lại. "Tôi nghĩ điều ấy quá hiển nhiên," sau rốt tôi
nói.
"Vậy sao?" Jerry trả lời. "Tao không được thuyết phục cho lắm. Hãy thử một lối
khác xem sao." Gã vớ lấy bàn phím và xoá hết mã nguồn của tôi. Tự ái trong lòng
bùng lên nhưng tôi cố dằn nó xuống. Dù gì cũng chỉ có vỏn vẹn bốn dòng code mà
thôi.
"OK, mình cần những hàm nào đây?" gã hỏi. Tôi nghĩ ngợi vài giây và nói, "mình
cần lấy tên hồ sơ từ dòng lệnh nhưng tôi không biết ông sẽ làm sao nếu không có
phần mã nguồn ông vừa xoá mất."
Jerry nhìn tôi với vẻ chế giễu, hắn nói, "tao biết," và bắt đầu gõ phím. Ðầu
tiên gã viết một đoạn test framework quen thuộc:
|
|
import
junit.framework.*;
public class TestSMCRemoteClient extends
TestCase {
public TestSMCRemoteClient(String name) {
super(name); }
} |
|
Gã biên dịch và chạy thử, nắm chắc phần test phải hỏng vì thiếu tests, và rồi
gã thêm đoạn test sau:
|
|
public void testParseCommandLine()
throws Exception {
SMCRemoteClient c = new SMCRemoteClient();
c.parseCommandLine(new String[]{“filename”});
assertEquals(“filename”,
c.filename());
} |
|
"OK," tôi nói. "Có vẻ như ông lấy đối số của dòng lệnh bằng function
parseCommandLine thay vì dùng main,
nhưng phiền như thế làm gì?"
"Thế để tao có thể thử nghiệm," Jerry cố nín cười, trả lời.
"Nhưng chẳng có gì để mà thử cả," tôi cằn nhằn.
"Ðiều đó có nghĩa quá hời để viết phần test," gã cười toe toét.
Tôi biết tôi sẽ không thắng nổi trận đấu này nên đành thở dài, vớ lấy bàn phím
và viết đoạn mã sau để phần test có thể đạt:
|
|
public class SMCRemoteClient {
private String itsFilename;
public void parseCommandLine(String[] args) {
itsFilename = args[0]; }
public String filename() {
return itsFilename; }
} |
|
Jerry gật đầu và lặng lẽ viết phần test case kế tiếp.
|
|
public void
testParseInvalidCommandLine() {
SMCRemoteClient c = new SMCRemoteClient();
boolean result = c.parseCommandLine(new
String[0]);
assertTrue(“result should be false”,
!result);
} |
|
Lẽ ra tôi phải biết gã chỉ cho tôi lý do tại sao tôi nghĩ, viết một cái test
không cần thiết lại là một khái niệm hay. "OK", tôi thú nhận. "Tôi đoán việc lấy
đối số của dòng lệnh ít vụn vặt hơn là tôi nghĩ. Có lẽ nó đáng để có một cái
test cho riêng nó." Thế rồi tôi vớ lấy bàn phím và làm cho phần test đạt.
|
|
public boolean parseCommandLine(String[]
args) {
try {
itsFilename = args[0];
} catch (ArrayIndexOutOfBoundsException e) {
return false; }
return true;
} |
|
Cân nhắc kỹ lưỡng, tôi refactor biến số c và khởi động
nó trong function setUp. Các tests đều đạt. Trước khi
Jerry có thể đề nghị phần test case tiếp theo, tôi nói, "Rất có khả năng hồ sơ
không tồn tại. Chúng ta nên viết một cái test chứng minh mình có thể lo cho
trường hợp ấy được."
"Quả vậy," Jerry nói trong khi tóm lấy bàn phím trong tay tôi. "Nhưng để tao chỉ
cho mà cách tao khoái làm thế nào."
|
|
public void testFileDoesNotExist()
throws Exception {
c.setFilename(“thisFileDoesNotExist”);
boolean prepared = c.prepareFile();
assertEquals(false,
prepared);
} |
|
"Mày thấy không?" gã giảng giải. "tao muốn ước định mỗi đối số của dòng lệnh
trong function của chính nó thay vì nhập chung cả mớ mã phân tích và ước định
chung với nhau." Trong khi đó, tôi kín đáo đảo mắt ráng ghi nhớ những điếm ấy để
tham khảo sau này, tôi lấy bàn phím và thay đổi những điểm sau để làm cho phần
test đạt:
|
|
public void setFilename(String
itsFilename) {
this.itsFilename = itsFilename; }
public boolean prepareFile() {
File f = new File(itsFilename);
if (f.exists()) {
return true;
} else
return false;
} |
|
Trọn bộ các test đều đạt. Jerry nhìn tôi rồi nghía sang bàn phím. Hiển nhiên gã
muốn "lái" bàn phím. Hôm nay dường như gã tràn đầy sáng kiến, bởi thế tôi chuyển
bàn phím về phía gã.
"OK, bây giờ xem đây!" gã nói, cỗ máy trong gã rõ ràng đang gầm rú.
|
|
public void testCountBytesInFile()
throws Exception {
File f = new File(“testFile”);
FileOutputStream stream = new FileOutputStream(f);
stream.write(“some text”.getBytes());
stream.close();
c.setFilename(“testFile”);
boolean prepared = c.prepareFile();
f.delete();
assertTrue(prepared);
assertEquals(9,
c.getFileLength());
} |
|
Sau khi nghiên cứu mã nguồn của gã vài giây, tôi trả lời, "Ông muốn
preparFile() để lấy độ dài của hồ sơ? tại sao?"
"Tao nghĩ lát nữa mình sẽ cần chúng," gã giải thích. "và đó là một cách hay để
chứng minh mình có thể đối phó với một hồ sơ hiện có."
"Mình cần nó để làm gì kia chớ?" tôi nằn nặc.
"Chúng ta sẽ phải gởi nội dung của hồ sơ xuyên qua socket đến server, phải
không?" Jerry hỏi.
"Vâng."
"Và chúng ta cần biết sẽ gởi bao nhiêu chữ," gã kiên nhẫn giải thích.
"Hườm... có lẽ," tôi miễn cưỡng trả lời.
"Tin tao đi," gã mỉm cười. "tận cùng thì tao làm người hướng đạo cơ mà."
"OK, khỏi nói đến chuyện ấy," tôi trả lời một cách thiếu kiên nhẫn. "Tạo sao ông
lại tạo hồ sơ trong phần test kia chớ? sao ông không giữ hồ sơ này sẵn thay vì
lần nào cũng phải tạo nó ra?"
Jerry cười khẩy rồi trở nên nghiêm túc. "Tao ghét giữ lại các nguồn bên ngoài
cho mấy cái test. Bất cứ khi nào có thể được, tao để cho mấy cái test tạo ra
nguồn chúng cần. Với cách ấy, không cách nào tao bị mất nguồn cả, hoặc ngay cả
trường hợp nguồn bị hỏng nữa."
"Ồ, điều này thì quả có lý," tôi thừa nhận, "nhưng tôi vẫn không điên khùng với
mấy thứ độ dài của hồ sơ kia."
"Nhớ đó. Mày sẽ thấy!"
Tôi lấy bàn phím và bắt đầu làm việc với phần cho phép đoạn test đạt. Trong khi
tôi gõ phím, tôi thấy hơi lạ vì tôi đang viết mã chính trong khi thiết kế là của
Jerry - nhưng những gì Jerry làm chỉ là viết những đoạn test case nhỏ. Bạn có
thể thực sự xếp loại một thiết kế bằng cách viết những test case hay không?
|
|
public long getFileLength() {
return itsFileLength;
}
public boolean prepareFile() {
File f = new File(itsFilename);
if (f.exists()) {
itsFileLength = f.length();
return true;
} else
return false;
} |
|
Test case kế tiếp tạo ra một cái server giả và dùng để thử khả năng của
SMCRemoteClient truy cập vào đó.
|
|
public void
testConnectToSMCRemoteServer()
throws Exception {
SocketServer server = new SocketServer(){
public void serve(Socket socket) {
try {
socket.close();
} catch (IOException e) {
}
}
};
SocketService smc = new SocketService(SMCPORT,
server);
boolean connection = c.connect();
assertTrue(connection);
} |
|
Với rất ít trở ngại, tôi làm cho phần test case đạt:
|
|
public boolean connect() {
try {
Socket s = new Socket(“localhost”,
9000);
return true;
} catch (IOException e) {
}
return false;
} |
|
"Tuyệt!" Jerry nói. "Mình nghĩ giải lao một tí."
"OK," tôi trả lời, "nhưng hãy viết phần main() trước
đã."
"main() gì, dính dự gì ở đây?" gã hỏi.
"Hở? đó là main của chương trình chớ gì!"
"Thế thì sao chớ?" Jerry rụt vai. "Nó chỉ gọi
parseCommandLine(), parseFile() và
connect(). Còn lâu lắm mình mới test mấy thứ đó!"
Tôi rời phòng làm việc và đi về phía phòng giải lao. Trước giờ tôi cứ nghĩ
main() là function đầu tiên cần được viết, nhưng Jerry
rất đúng. Rốt cuộc, main() chỉ là một function khá
thiếu thú vị.
Còn tiếp.....
|