[Swift] Test-driven development

TDD

TDD (Test Driven Development) là một phương thức làm việc, hay một quy trình viết mã hiện đại. Lập trình viên sẽ thực hiện thông qua các bước nhỏ (BabyStep) và tiến độ được đảm bảo liên tục bằng cách viết và chạy các bài test tự động (automated tests). Quá trình lập trình trong TDD cực kỳ chú trọng vào các bước liên tục sau:

  • Viết test case
  • Chạy test và check nếu test fail (lần đầu tiên luôn fail vì chưa có code được viết)
  • Viết code để chạy đúng test case
  • Chạy lại test
  • Refactor code
  • Lặp lại các bước trên

TDD có ưu điểm là giúp chúng ta giảm thiểu được việc thiếu các case logic lúc viết code, giúp code clean hơn. Nếu thêm mới, sửa. refactor code thì chúng ta vẫn có thể đảm bảo được các chức năng có sẵn trước đấy vẫn chạy ổn. Tuy vậy việc viết thêm test case cũng đồng nghĩa với việc bạn sẽ phải tốn thêm thời gian, thậm chí là tốn hơn rất nhiều lần so với việc code.

TDD Rules

  1. Không cho phép viết bất kỳ một mã chương trình nào cho tới khi nó làm một test bị fail trở nên pass.
  2. Không cho phép viết nhiều hơn một unit test mà nếu chỉ cần 1 unit test cung đã đủ để fail. Hãy chuyển sang viết code function để pass test đó trước.
  3. Không cho phép viết nhiều hơn 1 mã chương trình mà nó đã đủ làm một test bị fail chuyển sang pass.

Acceptance TDD

  • Mức chấp nhận (Acceptance TDD (ATDD)): với ATDD thì bạn viết một test chấp nhận đơn (single acceptance test) hoặc một đặc tả hành vi (behavioral specification) tùy theo cách gọi của bạn; mà test đó chỉ cần đủ cho các mã chường trình sản phẩm thực hiện (pass or fail) được test đó. Acceptance TDD còn được gọi là Behavior Driven Development (BDD).

  • Mức lập trình (Developer TDD): với mức này bạn cần viết một test lập trình đơn (single developer test) đôi khi được gọi là unit test mà test đó chỉ cần đủ cho các mã chường trình sản phẩm thực hiện (pass or fail) được test đó. Developer TDD thông thường được gọi là TDD.

Demo

Để tạo 1 demo đơn giản, thay vì tạo 1 project và add test, chúng ta có thể sử dụng luôn Playground cho đơn giản

Test case: 1 Tính sum 2 số từ 1 chuỗi string "2,3" 2 Hỗ trợ cả string có hơn 2 số 3 Với chuỗi rỗng -> trả về 0

Case 1 : Tính sum 2 số từ 1 chuỗi string "2,3"

struct Calculator {
    func sum(string: String) -> Int {
        return 0
    }
}

class TestCalculator {
    func testTwoNumberAndReturnValue() {
        let stringInput = "2,3"
        let expertResult = 5
        let calculator = Calculator()
        let actualResult = calculator.sum(string: stringInput)
        XCTAssert(actualResult == expertResult, "Sum not right")
    }
}

// Run test
let tester = TestCalculator()
tester.testTwoNumberAndReturnValue() // Fail

Lần đầu tiên run test -> Luôn fail -> Fix

struct Calculator {
    func sum(string: String) -> Int {
        let numbers = string.components(separatedBy: ",")
        let numberArray = numbers.map { (string) -> Int in
            return Int(string) ?? 0
        }
        return numberArray[0] + numberArray[1]
    }
}

// Run test
let tester = TestCalculator()
tester.testTwoNumberAndReturnValue() // Success

Tương tự với các case còn lại. Lưu ý là để đảm bảo code mới không gây ảnh hưởng tới code đã chạy, chúng ta vẫn phải chạy tất cả các test

Case 2: Support nếu có hơn 2 số trong string

    func testMoreThan2NumberAndReturnValue() {
        let stringInput = "2,3,4"
        let expertResult = 9
        let calculator = Calculator()
        let actualResult = calculator.sum(string: stringInput)
        XCTAssert(actualResult == expertResult, "Not support for more than 2 number")
    }
    
    // Run test
    let tester = TestCalculator()
    tester.testMoreThan2NumberAndReturnValue() // Fail

==> Sửa code trong Calculator

    func sum(string: String) -> Int {
        let numberStrings = string.components(separatedBy: ",")
        let numberArray = numberStrings.map { (string) -> Int in
            return Int(string) ?? 0
        }
        return numberArray.reduce(0, { (sum, number) -> Int in
            sum + number
        })
    }
    
    // Run test
    let tester = TestCalculator()
    tester.testMoreThan2NumberAndReturnValue() // Success

Case 3 : Empty string

    func testEmptyInputStringAndReturnValue() {
        let stringInput = ""
        let expertResult = 0
        let calculator = Calculator()
        let actualResult = calculator.sum(string: stringInput)
        XCTAssert(actualResult == expertResult, "Not support for empty string")
    }
    
// Run test
let tester = TestCalculator()
tester.testEmptyInputStringAndReturnValue() // Success

Case này đã pass nên chúng ta sẽ không cần phải sửa lại code

Kết

Trên đây là 1 demo nhỏ đơn giản về việc áp dụng TDD với Swift. Sau khi pass hết các testcase, các bạn có thể tiến hành refactor code (tham khảo https://refactoring.guru/ ) để code được sáng và đẹp hơn

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