Ví dụ về Test-Driven Development

Đây là một số ví dụ về Test-Driven Development mà mình đã tập hợp được.

Ngắn:

  • Chapter 6 – Agile principles, patterns, and practices in C# – by Martin C. Robert, Martin Micah. Khá thú vị. Xem online tại đây.
  • Phần 3, 4, 5 của Craftsman.

Trung bình:

  • Part I – Test-Driven Development by example – Kent Beck.
  • Part III – Test-Driven Development: A practical guide – David Astels.
  • Phần 6, 7, 8, 9, 10 của Craftsman.

Dài:

  • Part II – Test-Driven Development in Microsoft .NET – James W. Newkirk, Alexei A. Vorontsov.
  • Part III – Growing object-oriented software, guided by test – Steve Freeman, Nat Pryce.

Đặc biệt, nếu bạn có thời gian, không thể bỏ qua bộ Craftsman.

Advertisements

Đọc tài liệu tiếng Anh

HM viết bài này bởi vì có nhiều từ khóa tìm kiếm bằng tiếng Việt vào blog này quá, và hầu hết là về các vấn đề đơn giản, và ít bám theo liên kết trong blog để đến các bài viết tiếng Anh chi tiết hơn. Đây là một việc rất không tốt, bởi vì là lập trình viên thì càng cần phải biết dùng tiếng Anh để đọc và tra cứu tài liệu.

Trước hết phải dạo qua bước thủ tục đã. Tại sao lập trình viên nên dùng tài liệu tiếng Anh (chứ không phải là tiếng Việt)?

  • Sách tiếng Anh (tất nhiên là những quyển hay nhất) có chất lượng tốt hơn sách tiếng Việt. Nói chính xác hơn, sách lập trình tiếng Việt đa phần rất tệ (và rất mỏng), chỉ lướt qua các kiến thức bề mặt chứ ít khi đi sâu vào cốt lõi của vấn đề. Ngoại lệ có lẽ là các sách dịch.
  • Phần lớn các công nghệ phổ biến đều được phát triển bởi các công ti Mĩ. Tài liệu hỗ trợ vì thế sẽ viết bằng tiếng Anh là chủ yếu. Nếu có dịch sang tiếng khác thì cũng còn lâu tiếng Việt mới tới lượt.
  • Stack Overflow. Đây là trang web hỏi đáp nổi tiếng nhất cho lập trình viên. Ngay cả những vấn đề chưa được tài liệu hóa thì bạn cũng có cơ hội tìm được câu trả lời ở đây. Thậm chí cỗ máy tìm kiếm của MSDN cũng lấy các kết quả từ Stack Overflow. Tiếng Anh là ngôn ngữ duy nhất được dùng trên trang này.
  • Sách điện tử tiếng Anh rất dễ kiếm, đủ đề tài từ lập trình đến dạy hẹn hò 😀 (tất nhiên, tải sách điện tử “lậu” là không hợp pháp).

Vậy làm sao để đọc tài liệu tiếng Anh? Thứ nhất là phải học tiếng Anh. Nếu học nghiêm túc thì chắc bằng A là đủ rồi. Thứ hai, phải luyện tập. Bạn cần phải:

  • Bớt thời gian tìm kiếm sách học tiếng Anh, rồi lời khuyên học tiếng Anh, v.v đi. Chỉ có luyện tập mới tạo nên sự hoàn thiện.
  • Tập đọc tài liệu. Tất nhiên HM không thể có dẫn chứng xác thực, nhưng có rất nhiều sinh viên mắc bệnh lười đọc, hiếm khi đọc trọn vẹn giáo trình cho môn học, mà chỉ phụ thuộc vào bài giảng của giáo viên. Nếu thi trắc nghiệm thì có khi chỉ cần giải đề năm trước và học thuộc là đã có thể qua (và qua với điểm khá) :D. Chỉ nhìn cái gì nhiều chữ đã nản thì còn tính gì đến tiếng Anh tiếng Việt, hơn nữa tài liệu tiếng Anh lúc nào cũng dài, chi tiết, dày đặc chữ. Tập càng sớm càng tốt, ngay từ môn học đầu tiên. Đừng sợ hãi những bài báo 8-9 trang, hay những quyển sách dày 400-500 trang, nếu nội dung của nó thật sự có ích. Đừng trông chờ một ai đó (kể cả Google) lúc nào cũng có thể trả lời cho bạn những câu hỏi kiểu như làm sao hiện thực thuật toán tìm đường ngắn nhất, thuật toán Euclid mở rộng là gì. Đừng phụ thuộc vào những bài giảng trên lớp. Hãy đọc sách và động não.
  • Đừng sợ tiếng Anh. Bạn sẽ không bao giờ đọc tốt tiếng Anh nếu bạn không đủ kiên nhẫn và quyết tâm đọc nó. Nhiều người mặc dù đã được học tiếng Anh khá bài bản nhưng vấn có tâm lí này. Chỉ cần cấu trúc của câu phức tạp một chút, chỉ cần đoạn văn dài một chút, là họ đã bỏ qua rồi. Đừng làm thế. Hãy kiên nhẫn đọc cho hiểu từ những câu thông báo ngắn trên màn hình đến những bài báo dài từ Wikipedia tiếng Anh. Cố gắng đừng đóng lại một trang web chỉ vì nó viết bằng tiếng Anh, hãy đóng nó chỉ khi nó không hữu ích.
  • Đọc tiếng Anh thật nhiều. Bạn có tin là thậm chí một quyển sách tiếng Anh cấp hai cũng giúp bạn cải thiện khả năng đọc tiếng Anh không? Khi đọc một câu tiếng Anh, đừng dịch nó qua tiếng Việt rồi mới hiểu nó, mà hãy cố gắng hiểu trực tiếp bằng tiếng Anh. Coi như những từ tiếng Anh đó chẳng qua là từ tiếng Việt. Chẳng hạn, apple là táo. Vậy táo là gì? Nếu bạn không định nghĩa được táo là gì, mà chỉ nghĩ táo là táo, thì cũng hãy nghĩ apple là apple thôi.
  • Đừng đọc cà nhắc. Tập trung vào cấu trúc của câu, xem câu đó nói gì, hơn là tập trung vào nghĩa của từng từ. Nếu nghĩa của từ đó không cần thiết cho việc hiểu cả câu thì lơ nó đi cũng được, khi gặp nhiều lần hãy tra. Nếu bạn phải dừng lại tra từ quá nhiều, bạn sẽ không tóm gọn được ý nghĩa của cả đoạn, và hơn nữa bạn cũng sẽ chóng chán đọc tiếng Anh.
  • Đừng xấu hổ. Nếu bạn chưa thạo thì bạn đọc chậm. Nếu không hiểu thì nói là không hiểu. Nhất là lúc đầu khi tập đọc tiếng Anh mà không dịch trung gian qua tiếng Việt mọi thứ sẽ rất khó khăn và mất nhiều thời gian. Cũng giống như tập gõ bàn phím mười ngón vậy, lúc đầu chắc chắn người tập sẽ gõ chậm hơn gõ “mổ cò”, nhưng sau cùng họ sẽ gõ nhanh hơn hẳn những người “mổ cò” (cộng với việc không cần nhìn bàn phím).
  • Đừng dừng lại. Ngay cả khi bạn đọc tiếng Anh như tiếng Việt, thì cũng không có nghĩa là bạn có thể dừng lại. Hãy đọc thêm tiếng Anh ở nhiều lĩnh vực khác, mở rộng vốn từ của mình, tập viết tiếng Anh nhiều hơn để tăng cường trình độ tiếng Anh của mình. Tại sao? HM không thể trả lời được, nhưng HM nghĩ sự tự mãn, dù nhỏ hay lớn, khi đã xuất hiện sẽ làm con người ta tiêu tan ý chí phấn đấu, trở nên sợ hãi và dừng bước trước những thử thách vượt quá khả năng.

Một số công cụ rất hữu ích:

  • Simple English Wikipedia. Wikipedia phiên bản chỉ dùng tiếng Anh đơn giản. Vì là Wikipedia chắc chắn sẽ có chủ đề thú vị với bạn.
  • Englishforums.com. Ở đây lúc nào cũng có giáo viên hoặc người bản xứ trả lời câu hỏi của bạn.
  • Lingoes. Từ điển, giống Babylon, nhưng miễn phí.

Mong là một ngày nào đó blog này sẽ không còn lượt truy cập nào nữa :D.

SLF4J và logback

Mặc dù unit test có thể giúp hạn chế lỗi khi phát triển phần mềm, với những chương trình phức tạp, sự cố là không thể tránh khỏi, nghĩa là vẫn phải có debug. Và việc này sẽ đơn giản hơn rất nhiều nếu trạng thái của chương trình tại các thời điểm khác nhau được ghi lại. Ngoài ra, các thông tin này có thể giúp cải tiến chương trình về giao diện sử dụng, tốc độ, v.v. Đó là lí do chúng ta có các logging framework.

Tại sao không dùng println cho việc logging? Bởi vì chúng quá thô sơ. Một giải pháp logging, có lẽ phải đáp ứng các yêu cầu sau:

  • Tùy biến:
    • Bật, tắt logging (toàn bộ hoặc theo loại thông điệp.
    • Vị trí xuất log (màn hình, tập tin, database, v.v)
    • Định dạng, lượng thông tin cần log
  • Thông tin có thể log:
    • Vị trí gọi log
    • Thời gian
    • Thông điệp, stack trace, v.v

Có nhiều logging framework trong Java, trong đó nổi tiếng nhất là Log4J. Để thuận tiện khi chuyển đổi giữa các logging framework, người ta phát triển facade chung cho chúng. Không may là cũng lại có hơn một facade 🙂 (Java là thế). Hiện nay, các dự án có vẻ đang ưa chuộng facade SLF4J. API của SLF4J được framework logback hiện thực trực tiếp (native implementation). Ngoài tính dễ sử dụng, SLF4J còn có một lợi thế là được phát triển bởi tác giả của Log4J—tất nhiên là người hiểu rõ các hạn chế của Log4J.

Dưới đây là một ví dụ nhỏ về SLF4J. Tài liệu hướng dẫn và cả javadoc của SLF4J và logback đã rất cụ thể và đầy đủ, nên nếu bạn muốn tìm hiểu sâu hơn thì đó là nơi tốt nhất để bắt đầu.

Mặc định của logback là log ra console:

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
public class HelloWorld { 
  public static void main(String[] args) { 
    Logger logger = LoggerFactory.getLogger("HelloWorld"); 
    logger.debug("Hello world."); 
  } 
}

Kết quả (console):

22:12:35.261 [main] DEBUG HelloWorld - Hello world.

Sửa đổi tập tin cấu hình (thường là logback.xml) và ta sẽ log được ra tập tin:

<configuration> 
  <appender name="FILE" class="ch.qos.logback.core.FileAppender"> 
    <file>hw.log</file> 
    <encoder> 
      <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern> 
    </encoder> 
  </appender> 
  <root level="debug"> 
    <appender-ref ref="FILE" /> 
  </root> 
</configuration>

Kết quả (trong tập tin hw.log):

2010-08-21 21:35:57,583 DEBUG [main] HelloWorld [HelloWorld.java:7] Hello world.

JUnit

Có lẽ đa số sinh viên chúng ta đều quen với việc hoàn tất chương trình, chạy thử, thấy sai, tìm và sửa lỗi, chạy thử, v.v. Điều này không có gì là sai trái, tuy nhiên nếu đợi đến khi hoàn tất chương trình rồi mới chạy thử, thì việc tìm và sửa lỗi sẽ tương đối vất vả. Quá trình này sẽ đơn giản hơn, nếu bạn có thể chia chương trình thành từng phần nhỏ, và test chúng một cách riêng rẽ. Đó là ý nghĩa của unit test.

Tuy nhiên, để unit test hiệu quả, chúng ta cần có các công cụ để tiến hành nó một cách tự động. Các công cụ này gọi là unit test framework. Với Java, framework nổi tiếng nhất là JUnit.

Nếu bạn muốn tìm hiểu về JUnit, thì bạn nên đọc “JUnit in action”. Dưới đây là một ví dụ nhỏ về JUnit để bạn tham khảo.

Kiểm tra với một test case

Giả sử chúng ta có một class Number như dưới đây. Mỗi object của class đại diện cho một số nguyên. Method isEven kiểm tra tính chẵn lẻ của số nguyên đó. Mục tiêu của chúng ta là kiểm tra tính đúng đắn của method isEvent.

public class Number { 
    private int number; 
    public Number(int number) { this.number = number; } 
    public boolean isEven() { return (number % 2 == 0)?true:false; } 
}

Việc đầu tiên là tải tập tin junit-4.x.x.jar và cho nó vào CLASSPATH. Đoạn mã dưới đây sẽ kiểm tra isEvent với 2.

import static org.junit.Assert.*; 
import org.junit.Test; 
public class NumberTest { 
    @Test public void testIsEven() { 
        Number number = new Number(2); 
        assertTrue(number.isEven()); 
    } 
}

Dịch hai đoạn mã trên và chạy lệnh java org.junit.runner.JUnitCore NumberTest. Khi đó JUnit sẽ chạy các method được đánh dấu @Test. Kết quả là:

JUnit version 4.8.2 
. 
Time: 0.008 
OK (1 test)

Kiểm tra với nhiều test case

Bây giờ ta muốn kiếm tra với nhiều số hơn. Chúng ta sẽ không chèn thêm assertTrue vào testIsEven (vì như vậy sẽ không thể biết được số nào làm cho test bị sai). Thay vào đó, chúng ta sẽ tham số hóa method testIsEven. Các tham số được lấy từ bộ test tạo ra bởi method getTestParameters.

import static org.junit.Assert.*; 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.junit.runners.Parameterized; 
import org.junit.runners.Parameterized.Parameters; 
import java.util.Arrays; 
import java.util.Collection;  

@RunWith(value=Parameterized.class) 
public class NumberParameterizedTest { 
    private int value; 
    private boolean expected; 
    public NumberParameterizedTest(int value, boolean expected) { 
        this.value = value; 
        this.expected = expected; 
    } 
    @Parameters public static Collection&lt;Object[]&gt; getTestParameters() { 
        return Arrays.asList(new Object[][] { {2, true}, {3, false}, {5, true}, }); 
    } 
    @Test public void testIsEven() { 
        Number number = new Number(value); 
        assertEquals(number.isEven(), expected); 
    } 
}

Lần này, Junit sẽ lần lượt lấy từng mẫu ra để kiểm tra. Kết quả là (trích):

JUnit version 4.8.2 
...E 
Time: 0.022 
There was 1 failure: 
1) testIsEven[2](NumberParameterizedTest)...

Như vậy là test sai với mẫu thứ ba (5, true) (vì đếm từ 0).

Kiểm tra Exception

Trong một số trường hợp, một method sẽ throw Exception thay vì trả về kết quả tính toán. Để đảm bảo hành vi của method đúng như mong đợi, chúng ta phải test cả những trường hợp này để xem chúng có throw đúng Exception cần throw không. @Test(expected=YourException.class) là annotation phục vụ cho mục đích này.

Dùng test làm hướng dẫn sử dụng

Hãy nhìn lại đoạn code trên. Khi đọc test (nếu bạn hiểu JUnit), bạn sẽ suy ra ngay cách sử dụng method isEven: đầu tiên tạo một object Number với constructor nhận vào giá trị int, sau đó gọi isEven(). Vậy là, test code còn một tác dụng khác là làm hướng dẫn sử dụng (tương tự như các đoạn code trong tutorial trên mạng). Ưu điểm của việc lấy test làm hướng dẫn sử dụng là nhanh gọn và bám sát theo thay đổi của chương trình.

Local class trong Java

Local class là class được khai báo bên trong một method. Các object của class này có đặc điểm là chỉ truy xuất được variable của method nếu chúng là final. Để giải thích điều này, hãy xét đoạn mã dưới đây.

class Outer {
	SomeClass o;
	public void OuterMethod() {
		int a;
		class Inner {
			public void InnerMethod() {
				// Do something
			}
		}
		Inner i = new Inner;
		o = new SomeClass(i);
	}
}

Ở đây, i và a là các local variable của method OuterMethod. Khi method này kết thúc, i và a sẽ bị xóa khỏi stack. Thế còn object mà i trỏ tới liệu có bị Garbage Collector xóa? Câu trả lời là tùy từng trường hợp. Vì i đã được pass cho object o, nếu o lưu lại một reference để trỏ tới object mà i đang trỏ tới, thì object này chưa đủ điều kiện để bị thu hồi vùng nhớ. Điều đáng nói ở đây là object mà i trỏ tới có thể tồn tại lâu hơn method OuterMethod. Bây giờ, nếu trong phần mã của InnerMethod có một method nào đó sử dụng i, và method này được chạy khi method OuterMethod đã kết thúc (nghĩa là i đã bị xóa), thì nó sẽ lấy i ở đâu ra? Tất nhiên, nó không thể lấy được i ở đâu cả. Để tránh tình huống này, Java không cho mã của method-local inner class truy xuất variable của method chứa nó, trừ khi variable là final.

Xét tiếp phần “final” của vấn đề. Vì final variable không thể bị thay đổi một khi đã được gán giá trị, cho nên object của inner class có thể copy lại. Vậy tại sao không copy các variable còn lại? Rất đơn giản, nếu biến int i được copy, thì khi đó i của object và i của method không liên hệ gì với nhau, mọi thay đổi của i trong object không ảnh hưởng gì đến i của method, nghĩa là việc copy i cũng chỉ tương đương với việc chúng ta truyền i vào trong object thông qua constructor hoặc method bình thường, không hơn không kém. Hoàn toàn không cần thiết phải đưa thêm một tính năng khi nó không thực sự giúp giải quyết vấn đề gì.

Static/dynamic typing và strong/weak typing

Đôi khi người ta hay nhầm lẫn giữa static/dynamic typing và strong/weak typing. Phổ biến nhất là việc mặc nhiên xem các ngôn ngữ static typing là strong typing, và ngôn ngữ dynamic typing là weak typing. Thực tế đây là hai cách phân loại kiểm tra kiểu (type checking) khác nhau.

Static/dynamic typing

Đây là cách phân loại dựa vào thời điểm thực hiện kiểm tra kiểu.

Một ngôn ngữ lập trình thuộc dạng static typing (định kiểu chặt?) khi phần lớn công việc kiểm tra kiểu được thực hiện lúc biên dịch. Do khi biên dịch ta không thể xác định được giá trị của biến tại vị trí bất kì trong chương trình, trình biên dịch ngôn ngữ static typing phải gán kiểu cho biến ngay từ đầu (nhờ lập trình viên khai báo hoặc tự xác định khi biến được khởi tạo).

Trong khi đó, ngôn ngữ dạng dynamic typing kiểm tra kiểu lúc runtime. Kiểu của biến được xác định dựa vào giá trị của nó lúc sử dụng. Do đó, việc gán kiểu cho biến lúc biên dịch là không cần thiết (gán cũng được, nhưng như thế thì không cần đến dynamic typing nữa).

C là ngôn ngữ static typing, còn Python là ngôn ngữ dynamic typing. Để hiểu thêm, so sánh sự khác nhau giữa var (static typing) và dynamic (dynamic typing) trong C#.

Strong/weak typing

Khi kiểu của tham số trong một biểu thức không phù hợp, trình biên dịch phải chuyển tham số này thành kiểu đúng, rồi mới tính giá trì biểu thức. Tùy thuộc vào khả năng chuyển đổi giữa các kiểu với nhau mà chúng ta coi ngôn ngữ lập trình là strong typing hay weak typing. Ví dụ, C được xem là weak typing so với Java (nhớ là trong C ta có thể viết if (1) {} nhưng trong Java thì không). Python và Perl đều là ngôn ngữ dynamic typing, nhưng Perl được xem là weak typing so với Python (nhớ là biểu thức 1+”1” chỉ có ý nghĩa trong Perl).

Nói chung cách phân chia strong/weak này không có tiêu chuẩn gì, vì vậy ít được dùng một cách nghiêm túc. Tuy nhiên nhầm lẫn giữa hai cách phân loại static/dynamic và strong/weak thì cũng không hay chút nào.