Bài đăng nổi bật

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.

Post a Comment

أحدث أقدم