Tdd la gi

Trong phạm trù “phần mềm học” thì testing là một bước trong quy trình phát triển phần mềm. Mục đích của testing là đảm bảo phần mềm chạy đúng theo yêu cầu.

Tùy vào từng quy trình phát triển phần mềm mà testing có thể xuất hiện ở các bước khác nhau. Như trong quy trình phát triển phần mềm “Thác nước”, thì testing (kiểm thử) nằm ở bước thứ 4.

Tdd la gi
Tdd la gi
Các bước trong quy trình phát triển phần mềm thác nước

Có thể bạn chưa biết

Testing thường được thực hiện sau khi đã có chương trình (sau khi code), nên khiến cho nhiều developer coi nhẹ bước này vì cho rằng “tại sao lại phải test một đoạn code đã chạy đúng”. Vì thế mà rất nhiều phần mềm được tạo ra mà không hề có bước testing.

Testing có thể thực hiện bằng sức người, tức là con người sẽ trực tiếp thao tác trên phần mềm để tìm lỗi. Hoặc cũng có thể thực hiện qua các “công cụ test tự động”, người dùng sẽ đưa ra các “kịch bản test” kèm kết quả mong muốn, nó sẽ chạy và so sánh kết quả thực tế và kết quả mong muốn để phát hiện lỗi.

II. Testing Driven Development

2.1 TDD là gì?

TDD (Testing Driven Development – Phát triển hướng kiểm thử) là một quy trình phát triển phần mềm mà bước kiểm thử được thực hiện trước bước phát triển (tức là test trước khi code).

Ấy mà khoan, chưa code thì lấy cái gì để mà test nhỉ 🤔 🤔 🤔 – Đây cũng chính là điểm khác biệt của TDD với quy trình phát triển phần mềm truyền thống. Bạn cứ đọc hết bài viết sẽ rõ.

2.2 TDD được thực hiện như thế nào?

TDD được thực hiện qua các bước sau:

  • Bước 1: Viết một “kịch bản test” (test case), và đảm bảo nó sẽ fail (vì chưa code gì cả, fail là chắc)
  • Bước 2: Code để pass qua test case đó
  • Bước 3: Bổ sung thêm test case mới
  • Bước 4: Tiếp tục code để pass qua test case mới
  • Bước 5: Lặp đi lặp lại các bước tương tự 3 – 4 cho đến khi nào pass hết các test case
  • Bước 6: Refactor code – điều chỉnh lại code cho gọn gàng, dễ hiểu, để người sau vào đọc đỡ chửi

Bạn sẽ hiểu rõ hơn khi chúng ta thực hiện ví dụ qua phần III.

III. Ví dụ về TDD

Lý thuyết vậy là đủ, giờ chúng ta sẽ thực hiện một ví dụ đơn giản theo quy trình TDD để hiểu rõ hơn về nó.

Bài toán

Viết một function truyền vào một số nguyên N và trả về:
– Trả về “Fizz” nếu N chia hết cho 3
– Trả về “Buzz” nếu N chia hết cho 5
– Trả về “FizzBuzz” nếu N chia hết cho cả 3 và 5
– Trả về N nếu là các trường hợp còn lại

Mình sẽ sử dụng PHP để triển khai bài toán trên, và sử dụng PHPUnit để làm công cụ test tự động.

Bước 1: Setup các thứ

Tạo thư mục làm việc và cài đặt phpunit.

mkdir tdd && cd tdd && mkdir src && mkdir tests
composer require --dev phpunit/phpunit ^9

Chỉnh sửa file

composer dump-autoload
5 cho giống như sau:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}

Chạy autoload

composer dump-autoload

Tạo file

composer dump-autoload
6 với nội dung như sau:

Tạo file

composer dump-autoload
7 với nội dung như sau:

Bước 2: Triển khai TDD

Chúng ta đã có đầy đủ “nguyên liệu”, giờ sẽ bắt đầu triển khai TDD.

Bước 1: Tạo test case đầu tiên và đảm bảo nó fail

Mình sẽ tạo test case cho trường hợp trả về chữ Fizz, nếu N chia hết cho 3 trước.

assertEquals($actual, $expect);
    }

}

Chạy thử bằng lệnh

./vendor/bin/phpunit tests

Và kết quả là:

PHPUnit 9.3.9 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 00:00.026, Memory: 4.00 MB
There was 1 failure:
1) FizzBuzzTest::testFizz
Failed asserting that 'Fizz' matches expected null.
/home/vagrant/www/pets/phpunit/tests/FizzBuzzTest.php:13
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Bạn thấy đó, nó sẽ báo Failures: 1, tức là có 1 test case bị fail. Đương nhiên, vì mình chưa code gì mà.

Bước 2: Code để pass test case đầu tiên

Chúng ta sẽ sửa lại code trong file

composer dump-autoload
6 như sau:

Bằng cách đơn giản nhất, mình chỉ cần

composer dump-autoload
9 thì chắc chắn là sẽ pass. Nhưng cứ chạy lại test cho chắc.

./vendor/bin/phpunit tests

Và kết quả là:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
0

Kết quả trên nghĩa là mọi test case đã pass.

Bước 3: Bổ sung thêm test case mới

Mình sẽ tạo tiếp test case cho trường hợp trả về Buzz nếu N chia hết cho 5.

Bổ sung thêm test case trong file

composer dump-autoload
7 như sau:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
1

Chạy thử

./vendor/bin/phpunit tests

Và kết quả là:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
3

Nó báo là test case testBuzz bị sai, kết quả mong muốn là Buzz nhưng kết quả thực tế lại là Fizz. Đương nhiên, vì hàm

1 luôn trả về chữ Fizz mà.

Bước 4: Tiếp tục code để pass qua test case mới

Chúng ta sẽ sửa lại code trong file

composer dump-autoload
6 như sau:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
4

Chạy lại xem kết quả thế nào

./vendor/bin/phpunit tests

Và kết quả là:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
6

Vậy là tất cả test case đã pass.

Bước 5: Lặp đi lặp lại các bước tương tự 3 – 4

Chúng ta còn 2 trường hợp nữa là:

– Trả về FizzBuzz khi N chia hết cho cả 3 và 5
– Trả về N nếu là các trường hợp còn lại

Chúng ta sẽ lần lượt triển khai thêm 2 trường hợp này.

Trả về FizzBuzz khi N chia hết cho cả 3 và 5

Bổ sung thêm test case trong file

composer dump-autoload
7 như sau:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
7

Kết quả chạy test sau khi có test case mới:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
8

Kết quả là fail và fail ở test case testFizzBuzz.

Sửa lại code trong file

composer dump-autoload
6 như sau:

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
9

Kết quả chạy lại sau khi sửa code là:

composer dump-autoload
0

OK, vậy là đã pass.

Trả về N nếu là các trường hợp còn lại

Tiếp tục bổ sung thêm test case:

composer dump-autoload
1

Kết quả chạy test sau khi có test case mới

composer dump-autoload
2

Kết quả là fail và fail ở test case testOthers.

Sửa lại code trong file

composer dump-autoload
6 như sau:

composer dump-autoload
3

Kết quả test sau khi sửa code:

composer dump-autoload
4

Vậy là đã pass tất cả các test case.

Bước 6: Refactor code

Refactor code là chỉnh lại code sao cho gọn gàng, dễ hiểu, vì trong quá trình bổ sung thêm code mới có thể làm code tổng thể trở nên rối rắm, khó hiểu. Nhưng vì ví dụ này quá đơn giản nên không có gì để refactor cả.

Lưu ý

Một số developer cho rằng “Code đang chạy thì không nên sửa”, nhưng với trường hợp này bạn đã có test case, và bạn chỉ cần đảm bảo sau khi sửa code vẫn pass hết các test case là được.

IV. Tổng kết

TDD đơn giản là viết test case trước và implement code sau, nghe qua tưởng “dở”, thế nhưng khi áp dụng thì lại thấy hay, bởi:

  • TDD tạo động lực cho developer viết test case, sẽ không còn suy nghĩ “Tại sao phải test một đoạn code đã chạy” khi mà bạn còn chưa code một dòng nào.
  • Bạn sẽ “thong thả” mà sửa code, không sợ sửa chỗ này “đá” chỗ kia, vì đã có test case đảm bảo.
  • Viết test trước khi code giúp bạn tập trung suy nghĩ vào kết quả đạt được hơn là cách triển khai, điều này giúp bạn có cái nhìn tổng quát về chương trình.

Một số lưu ý khi áp dụng TDD:

  • Không viết thì thôi, nếu viết test case thì phải bao hết các trường hợp, nếu không sẽ phản tác dụng.
  • Mục đích sau cùng vẫn là phần mềm chạy tốt, nên hãy ưu tiên TDD cho những tính năng quan trọng.
  • Test case cũng là code, vì thế bạn cũng phải dành thời gian để maintain code ở các test case nữa.
  • Đừng áp dụng TDD khi sản phẩm của bạn “quá đơn giản”, nhưng lưu ý, không áp dụng TDD chứ không phải là không có bước testing.

Sau cùng, liệu bạn có áp dụng TDD trong dự án tiếp theo?

Facebook

Twitter

Pinterest

WhatsApp

Phạm Bình

https://phambinh.net

Mình muốn chia sẻ các kiến thức mà mình học được trong quá trình làm việc và phát triển, với hy vọng có sẽ giúp ích được cho bạn đọc - như cách mà mình học được từ việc đọc các blog khác.