Diễn Đàn Tin Học » Tutorial Room » Lập trình » Software Engineering » Craftsman-Truyện dài nhiều tập Craftsman - 8 : Testing in Synch "Testing in Synch", tay học việc của chúng ta học được một điều: các tests có mục đích phục vụ lớn hơn là chỉ đơn thuần chứng minh là mã nguồn chạy được: "tests" là một dạng tài liệu thực hành và giáo dục.
"Testing in Synch", tay học việc của chúng ta học được một điều: các tests có
mục đích phục vụ lớn hơn là chỉ đơn thuần chứng minh là mã nguồn chạy được:
"tests" là một dạng tài liệu thực hành và giáo dục.
Robert C. Martin
Thang máy tầng 17 lại hỏng nên tôi phải dùng trụ tuột. Trong lúc tụt xuống, tôi
bắt đầu ngẫm nghĩ đến chuyện đáng ghi nhận là dùng tests như một thứ đồ nghề
thiết kế. Chìm đắm trong suy nghĩ, tôi hơi vô ý nên va cùi chỏ vào trụ thang với
sức dội Coriolis [*].
Khi tôi gặp Jerry trong phòng thí nghiệm nó vẫn còn đau nhói.
"Mày sẵn sàng thử cái test dùng để gởi thông điệp 'hello' xuyên qua sockets
chưa?" gã hỏi.
"Hiển nhiên rồi", tôi đáp.
Chúng tôi bỏ phần phụ chú (comment) của method
TestSendMessage.
| |
public void testSendMessage() throws
Exception {
SocketService ss = new SocketService();
ss.serve(999, new HelloServer());
Socket s = new Socket("localhost", 999);
InputStream is = s.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String answer = br.readLine(); s.close(); assertEquals("Hello",
answer);
} |
|
"Như dự đoán, đoạn này không biên dịch được" Jerry phát biểu. "Mình cần phải
viết cái HelloServer."
"Tôi nghĩ tôi biết phải làm gì," tôi trả lời. "HelloServer là
cái class thừa hưởng từ SocketServer và ứng dụng
method server() để gởi thông điệp 'hello' qua socket."
Tôi vớ lấy bàn đánh và điều chỉnh nhóm TestSocketServer.java như
sau:
| |
class HelloServer implements
SocketServer {
public void serve(Socket s) {
try {
OutputStream os = s.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("Hello");
} catch
(IOException e) { }
}
} |
|
Ðoạn này biên dịch được và các tests đều đạt ngay lần đầu.
"Ngon lành," Jerry nói. "Bây giờ chúng ta có thể gởi một thông điệp qua socket."
Tôi biết Jerry bắt đầu nghĩ đến chuyện refactoring và tôi muốn qua mặt gã. Xem
xét đoạn mã kỹ lưỡng, tôi nhớ gã có đề cập đến vấn đề trùng lặp.
"Có một số mã trùng lặp trong các phần unit tests," tôi nói. "Trong mỗi cái test
mình tạo và đóng cái SocketService. Chúng ta nên bỏ nó
đi."
"Tinh mắt lắm!" Jerry phán. "Hãy dời nó vào các function Setup và Teardown." Gã
tóm lấy bàn đánh và thay đổi như sau:
| |
private SocketService ss;
public void setUp() throws Exception {
ss = new SocketService();
}
public void tearDown() throws Exception {
ss.close();
} |
|
Sau đó gã bỏ trọn bộ các dòng ss = newSocketService(); and
ss.close(); trong ba cái tests.
"Xem được hơn đó," tôi nói. "Hãy thử xem mình có thể gởi một thông điệp ngược
lại không."
"Tao cũng nghĩ y như vậy," Jerry trả lời. "Và tao có một cách làm chuyện đó."
Gã bắt đầu đánh một test case mới:
| |
public void testReceiveMessage()
throws Exception {
ss.serve(999, new EchoService());
Socket s = new Socket("localhost", 999);
InputStream is = s.getInputStream();
InputStreamReader isr = new
InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
OutputStream os = s.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("MyMessage");
String answer = br.readLine();
s.close();
assertEquals("MyMessage", answer);
} |
|
"Eo ôi! Tởm thế," tôi cằn nhằn.
"Ừa, đúng thật," Jerry thú nhận. "Hãy làm cho nó chạy cái đã rồi mình dọn dẹp nó
sau. Chúng ta không muốn mớ lộn xộn đó ở đây lâu! Mày biết tao định làm gì phải
không?"
"Vâng," Tôi trả lời. "EchoService sẽ nhận một thông
điệp từ socket và gởi ngược lại ngay. Bởi thế, đoạn test của ông chỉ gởi
MyMessage; rồi đọc nó lại."
"Ðúng rồi. muốn thử ngoáy phần EchoService không?"
"Tất nhiên," tôi nói một cách hăm hở.
| |
class EchoService implements
SocketServer {
public void serve(Socket s) {
try {
InputStream is = s.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
OutputStream os = s.getOutputStream();
PrintStream ps = new PrintStream(os);
String token = br.readLine();
ps.println(token);
} catch (IOException e)
{ }
}
} |
|
"Oái," tôi nói. "Lại thêm một mớ mã xấu xí. Mình cứ tạo các objects
PrintStream và BufferedReader từ
socket. Chúng ta cần phải dọn dẹp mới được."
"Mình sẽ làm chuyện đó ngay sau khi mấy cái test chạy ngon lành," Jerry đáp,
trong khi nhìn tôi có vẻ kỳ vọng.
"Oh!" tôi rú lên. "Tôi quên chạy cái test." Xấu hổ, tôi nhấn nút test và theo
dõi nó chạy. "Cũng không khó lắm," tôi nói. "Bây giờ hãy vứt phần mã xấu xí ấy
đi." Tôi rút nhiều functions ra khỏi EchoService.
| |
class EchoService implements SocketServer {
public void serve(Socket s) {
try {
BufferedReader br = getBufferedReader(s);
PrintStream ps = getPrintStream(s);
String token = br.readLine();
ps.println(token);
} catch (IOException e) {
}
}
private PrintStream
getPrintStream(Socket s) throws IOException {
OutputStream os =
s.getOutputStream();
PrintStream ps = new
PrintStream(os);
return ps;
}
private BufferedReader getBufferedReader(Socket
s) throws IOException {
InputStream is =
s.getInputStream();
InputStreamReader isr =
new InputStreamReader(is);
BufferedReader br =
new BufferedReader(isr);
return br;
}
} |
|
"Ðoạn này cải tiến method EchoService," Jerry nói,
"nhưng nó khá rối cái class. Hơn nữa, nó không giúp gì cho function
testRecieveMessage, đó cũng là một điểm không đẹp. Thử
nghĩ getBufferedReader và
getPrintStream có nằm đúng chỗ không?"
"Ðây sẽ là vấn đề lặp lại," tôi nói. "Ai muốn dùng
SocketService phải sẽ chuyển socket thành
BufferedReader và PrintStream."
"Chính là câu trả lời!" Jerry đáp lại. "Các methods
getBufferedReader và getPrintStream quả thực
thuộc về SocketService."
Tôi dời hai functions vào class SocketService và thay
đổi EchoService theo đó.
| |
public class SocketService {
[...]
public static PrintStream getPrintStream(Socket
s) throws IOException {
OutputStream os =
s.getOutputStream();
PrintStream ps = new
PrintStream(os);
return ps;
}
public static
BufferedReader getBufferedReader(Socket s) throws IOException
{
InputStream is =
s.getInputStream();
InputStreamReader isr =
new InputStreamReader(is);
BufferedReader br =
new BufferedReader(isr);
return br;
}
}
class EchoService implements SocketServer {
public void serve(Socket s) {
try {
BufferedReader br = SocketService.getBufferedReader(s);
PrintStream ps = SocketService.getPrintStream(s);
String token = br.readLine();
ps.println(token);
} catch
(IOException e) { }
}
} |
|
Các tests đều chạy. Giữ vững tình hình, tôi nói: "bây giờ tôi hẳn có thể sửa đổi
method testReceiveMessage luôn." Trong lúc Jerry quan
sát, tôi thay đổi phần này như sau:
| |
public void testReceiveMessage() throws Exception {
ss.serve(999, new
EchoService());
Socket s = new Socket("localhost",
999);
BufferedReader br =
SocketService.getBufferedReader(s);
PrintStream ps = SocketService.getPrintStream(s);
ps.println("MyMessage");
String answer = br.readLine();
s.close();
assertEquals("MyMessage",
answer);
} |
|
"Ừa, coi được hơn đó," Jerry nói.
"Không chỉ như vậy mà các tests đều đạt," tôi gáy lên. Thế rồi tôi nhận thấy
thêm một điều. "Ui, có thêm một cái nữa trong testSendMessage."
Tôi sửa cái đó luôn.
| |
public void testSendMessage() throws Exception {
ss.serve(999, new
HelloServer());
Socket s = new Socket("localhost",
999);
BufferedReader br =
SocketService.getBufferedReader(s);
String answer = br.readLine();
assertEquals("Hello",
answer);
} |
|
Các tests vẫn chạy. Tôi ngồi tẩn mẩn xét lại class
TestSocketServer, tìm xem có gì loại bỏ được không.
"Mày xong chưa?" Jerry hỏi.
Tôi gật đầu.
"Tốt," gã đáp lại. "Mày sắp xịt khói lỗ tai đó."
"Tôi có một câu hỏi. Chúng ta chẳng thay đổi tí nào cái
SocketService. Mình thêm testSendMessage và
testRecieveMessage và cả hai đều chạy. Mình lại tốn
rất nhiều thời gian để viết mấy cái test và lo chuyện refactoring. Làm như thế
có lợi gì cho mình nhỉ? Chúng ta chẳng thay đổi mã nguồn chính của sản phẩm gì
hết!"
Jerry nhướn mày. "Bộ mày nghĩ là getBufferedReader và
getPrintStream đáng được đưa vào sản phẩm?" Mấy cái
này khá tầm thường; chúng chỉ hỗ trợ cho mấy cái test mà thôi.
Jerry thở dài. "Nếu mày dính vào project này, tao chỉ cho mày mấy cái test,
chúng dạy mày được những gì?"
Tôi học được gì từ mấy cái test đó? Tôi học được cách tạo
SocketService và gắn SocketServer từ đó. Tôi
cũng học được cách gởi và nhận thông điệp. Tôi học được tên và vị trí của các
classes trong framework và cách xử dụng chúng. "Ý ông mình viết mấy cái tests
này để làm ví dụ cho những người khác?"
"Ðó là một phần lý do, Alphonse. Những người khác sẽ có thể đọc mấy cái test này
và xem cách làm việc của mã nguồn. Họ cũng có thể làm việc xuyên qua lý giải.
Hơn thế, họ sẽ có thể biên dịch và thao tác các cái tests này và chứng minh với
chính họ rằng cách lý giải của chúng ta đáng thuyết phục. Còn nhiều điều hơn thế
nữa," gã nói tiếp, " nhưng chúng ta để dành vấn đề này cho một dịp khác."
Cùi chỏ tôi vẫn còn đau nhức nên tôi mừng là thang máy đã chữa. Trong khi đi
thang máy, tôi không ngừng nghĩ ngợi: "Tests là một dạng tài liệu-có thể biên
dịch được, có thể thao tác và luôn luôn đồng bộ."
--------------------------------------------
[*] Còn có tên gọi là Coriolis effect gọi theo tên của
kỹ sư - nhà toán học Pháp Gustave-Gaspard Coriolis. Ở đây dường như tác giả mô
tả hành động tay học việc ôm cột tụt xuống và trong khi tụt xuống, anh ta ở
trạng thái xoay vòng trên cột nên bị "Coriolis force". Xem thêm chi tiết ở:
http://zebu.uoregon.edu/~js/glossary/coriolis_effect.html. và
http://satftp.soest.hawaii.edu/ocn620/coriolis/ - (chú thích của người dịch).
[Trở
lên vị trí cũ]
|