SSV
0

Tìm hiểu mô hình TDD (Test - Driven Development) và cách áp dụng

I. Giới thiệu

Trong bài viết này mình xin thảo luận cùng các bạn về đề tài làm thế nào để implement code theo TDD. Mình xin trình bày bài viết theo hướng thực tế áp dụng TDD trong project mình đang tham gia hơn là một bài viết giới thiệu về lý thuyết TDD. Các bạn có thể tham khảo về lý thuyết TDD ở đây:

http://blog.co-mit.com/post/9/Tìm+hiểu+mô+hình+TDD+(Test+-+Driven+Development)+và+cách+áp+dụng

hoặc đơn giản hơn là google và search TDD 😄.

II. Bắt đầu TDD như thế nào?

Sau khi đọc xong bài giới thiệu khái niệm TDD ở trên, rất nhiều bạn có thể sẽ thắc mắc vậy chúng ta sẽ bắt đầu TDD như thế nào? Viết TDD bắt đầu từ đâu?

Để trả lời câu hỏi trên mình xin giới thiệu vài ví dụ về áp dụng TDD vào test app Java với Jmockit.

1. Ví dụ 1: bắt đầu từ yêu cầu nhỏ

Đề bài: Convert chuỗi string theo các quy tăc sau:

  1. Nếu chuỗi string có xuất hiện cụm từ trong "()", remove cụm từ này.
  2. Nếu chuỗi string kết thúc bằng một trong các cụm từ sau: "test", "tst", "st", "app", "application". Remove chúng cho đến khi chuỗi kết thúc bằng khác với cụm từ trên.
  3. Nếu chuỗi còn lại sau step 2 có lớn hơn 20 kí tự, xóa các từ ở cuối chuỗi cho đến khi còn lại chuỗi với độ dài < 20 kí tự.

Theo TDD, chúng ta viết test trước cho yêu cầu này như sau:

public class ImplementClass {

    public String convertString(String input) {
        // TODO Auto-generated method stub
        return null;
    }
}
public class ImplementClassTest {
	@Tested ImplementClass target;

	@Test
    public void convertStringIntoShortFormat() {
        new StrictExpectations(target){
        	target.removeWordsInBracket("input"); result = "after 1st step";
            target.removeWordsAtTheEnd("after 1st step"); result = "after 2nd step";
            target.getSubWordsFromBegin("after 2nd step", 20); result = "output";
        }
        assertThat(target.convertString("input"), is("output"));
    }
}

Ở đây @Tested là annotation cho class chúng ta cần test, nó có vai trò định hướng class chúng ta cần test là class gì. Với anotation @Tested, chúng ta có thể sử dụng @Injection: inject property với name và kiểu tương ứng trong class @Tested, @BeforeTest: run trước khi @Tested class khởi tạo, @AfterTest run sau khi class test call destructor v.v... Chúng ta sẽ bàn sau về những khái niệm này.

Các bạn có thể thấy bắt đầu với TDD khá là đơn giản, chúng ta chỉ cần đọc requirement và viết test behavior theo mô tả trong spec là ok. Ở đâu, chúng ta chưa cần quan tâm đến cụ thể function này sẽ làm như thế nào mà chúng ta chỉ cần quan tâm đến follow của nó cần chạy đúng như trong spec là đủ.

Lúc này test method của chúng ta sẽ fail vì chưa implemnt các method: removeWordsInBracket, removeWordsAtTheEnd, getSubWordsFromBegin. -> Việc chúng ta cần làm là follow theo suggestion error và create các method đó ra như sau: 😄

public class ImplementClass {

    public String convertString(String input) {
        // TODO Auto-generated method stub
        return null;
    }

    String removeWordsInBracket(String input) {
        // TODO Auto-generated method stub
        return null;
    }

    String removeWordsAtTheEnd(String input) {
        // TODO Auto-generated method stub
        return null;
    }

    String getSubWordsFromBegin(String input, int maxLength) {
        // TODO Auto-generated method stub
        return null;
    }
}

Run lại test case và lúc này đây test case sẽ báo như sau:

java.lang.AssertionError:
Expected: is "output" but: was null

Tất nhiên rồi, chúng ta cần implemnt để hoàn thiện code cho method convertString.

public String convertString(String input) {
        String afterFistStep = removeWordsInBracket(input);
        String afterSecondStep = removeWordsAtTheEnd(afterFistStep);
        return getSubWordsFromBegin(afterSecondStep, 20);
    }

Run test again và bây giờ code unit test cho method này đã pass. Việc tiếp theo là ta viêt test cho method chưa implement trước khi implement code cho nó.

Bắt đầu viết test cho removeWordsInBracket method. Ở đây mục đích của method đã rất rõ ràng, nên chúng ta sẽ test hàm trên bằng các case cụ thể và compare với giá trị trả về của method.

@Test
        public void removeWordsInBracketWhenHaveOneWord() throws Exception {
            assertThat(target.removeWordsInBracket("test word(test1)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word((test1)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test 2 (test1)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test1)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test1))"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test1)test2)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test1)"), is("test word"));
            assertThat(target.removeWordsInBracket("test word(test1) some word(test2)"), is("test word some word"));
            assertThat(target.removeWordsInBracket("test word(test1)some(test2)"), is("test word some"));
        }

        @Test
        public void keepOriginalWordWhenExistOnlyOpenBracket() throws Exception {
            assertThat(target.removeWordsInBracket("test word(test1"), is("test word(test1"));
            assertThat(target.removeWordsInBracket("test word(((test1) (test2)(some"), is("test word (some"));
        }

        @Test
        public void keepOriginalWordWhenExistOnlyCloseBracket() throws Exception {
            assertThat(target.removeWordsInBracket("test word test1)"), is("test word test1)"));
            assertThat(target.removeWordsInBracket("test word(((test1) (test2)(some"), is("test word (some"));
        }

Sau đó chúng ta implement code cho method removeWordsInBracket cho đến khi pass hết test case (hehe)

Khi chúng ta implement và pass hết test case cho method removeWordsInBracket chúng ta chuyển qua implement cho method removeWordsAtTheEnd, và getSubWordsFromBegin.

Sau đó chúng ta có thể từ từ improve implement code lên dần dần. Mỗi khi chúng ta thay đổi code implement hoặc thấy phát sinh thêm test case mới, chúng ta add thêm code vào class test và chạy lại.

2. Tổng kết nho nhỏ từ ví dụ 1

Viết code theo TDD sẽ trở lên đơn giản nếu chúng ta code bám sát spec hoặc design, tư duy một cách tự nhiên, tuần theo các nguyên tắc đầu tiên của SOLID (Một class chỉ nên giữ 1 trách nhiệm duy nhất). 4 nguyên tắc sau phụ thuộc vào quá trình design class, tạm thời ta không đề cập ở đâu, vì nếu viết TDD với quá nhiều nguyên tắc sẽ dẫn đến mất tự nhiên, gò bó và làm tự làm khó mình ra. Cứ tuân theo quy tắc 1 class, 1 method 1 mục đích là được. Nếu chúng ta chỉ viết dùng assertion test cho cả method bao ngoài, thì việc viết test case sẽ trở lên rât phức tạp, và nếu có sai sót xảy ra thì khó có thể sử dụng unit test để debug nhanh được.

Viết theo quy tắc sử dụng behavior test để test follow logic và dùngf thi assertion test để test cho expection result cụ thể mình mong muốn.Ví dụ khi ở đây minh thiếu case check null value cho dữ liệu đầu vào -> khi chạy chương trình báo "null pointer error exception." Chúng ta cần bổ sung test case cho null value input vào test và chạy lại test case.

@Test
        public void convertStringReturnNullWhenInputValueNull(){
        assertThat(target.convertString(null), is(nullValue()));
}
public String convertString(String input) {
		if(input == null){
        	return null;
        }
        String afterFistStep = removeWordsInBracket(input);
        String afterSecondStep = removeWordsAtTheEnd(afterFistStep);
        return getSubWordsFromBegin(afterSecondStep, 20);
    }

Với unit test được viết đủ các case, mỗi khi change code chúng ta không sợ ảnh hưởng đến logic cũ. Mỗi khi có một case mới được phát hiện ra và chúng ta cần change code cũng cần viết thêm test case và implement lại code sau khi viết test -> tránh không xảy ra việc thiếu case test.


All Rights Reserved