Unit Tests in Swift

by DaoNM2
255 views

Bạn đang tìm một phương pháp để tăng chất lượng source code? Bạn đang gặp vấn đề về việc source code của bạn có quá nhiều bug? Unit tests là một trong những lựa chọn giúp bạn hạn chế vấn đề đó.

Hiện nay rất nhiều dự án yêu cầu viết Unit tests nhằm mục đích đảm bảo chất lượng source code, vì vậy bài viết này mình sẽ chia sẻ với các bạn về Unit Tests trong Swift để các bạn có thể trang bị cho mình được một kĩ năng mới, để có thể sẵn sàng và tự tin chiến các dự án hiện tại hoặc trong tương lai.

Unit Testing là gì?

Unit tests là tự động chạy và kiểm thử một đoạn mã để đảm bảo nó hoạt động đúng như dự định và đúng với tài liệu yêu cầu.

Unit tests trong ngôn ngữ lập trình là việc viết các func test để đảm bảo source code hoạt động đúng như tài liệu yêu cầu. Với một đầu vào cụ thể sẽ cho ra một đầu ra cụ thể như tài liệu yêu cầu. Việc viết Unit tests để kiểm tra source code của bạn giúp bạn tự tin hơn khi release hay tái cấu trúc source code, vì bạn sẽ đảm bảo source code của mình chạy đúng mong đợi khi bạn chạy bộ test case của bạn thành công.

Các quan điểm trái ngược về Unit tests

Hiện nay có rất nhiều quan điểm trái ngược nhau về việc một dự án có cần phải viết Unit Tests hay không? Rất nhiều Developer thì cho rằng việc viết test tốn quá nhiều thời gian nó làm ảnh hưởng tới việc bàn giao công việc đúng thời hạn. Một số Developer thì cho rằng việc viết Unit tests không đem lại quá nhiều lợi ích mà công việc lại lặp đi lặp lại quá nhàm chán. Tuy nhiên theo mình nếu bạn viết Unit tests đúng cách thì sẽ giúp bạn giảm được thời gian phát triển ứng dụng, tuy thời gian phát triển ban đầu có tăng thêm nhưng bạn sẽ giảm được số lượng bug cơ bản có thể xảy ra từ đó giảm thời gian fix bugs và hạn chế các lỗi phát sinh sau khi sửa.

Viết Unit Tests với Xcode

Để giúp các nhà phát triển có thể viết Unit Tests cho các ứng dụng của họ, Apple đã tạo ra XCTest framework. Giờ đây các nhà phát triển có thể sử dụng framework này đê viết Unit tests cũng như chạy các test case để kiểm tra chất lượng source code của họ.

Một số hàm dùng để kiểm tra của XCTest Framework

  1. XCTAssert(): Nó khá thông dụng có thể sử dụng trong hầu hết các trường hợp, VD: XCTAssert(result == 5)
  2. XCTAssertTrue(): test case của bạn sẽ pass nếu biểu thức kiểm tra có kết quả là true. VD: XCTAssertTrue(view.isHidden)
  3. XCTAssertEqual(a, b), XCTAssertNotEqual: Kiểm tra xem giá trị của 2 biểu thức.
  4. XCTAssertFalse(): ngược với XCTAssertTrue
  5. XCTAssertGreaterThan(a, b): Thường dùng khi bạn kiểm tra 2 giá trị số
  6. XCTAssertGreaterThanOrEqual(a, b): tương tự như XCTAssertGreaterThan, nếu 2 giá trị = nhau thì test case vẫn pass
  7. XCTAssertLessThan, XCTAssertLessThanOrEqual: Tương tự như mục 5 và 6
  8. XCTAssertNil(a), XCTAssertNotNil: Dùng khi cần kiểm tra một var/func có nil hay không
  9. XCTAssertNoThrow() Dùng khi cần kiểm tra xem func có throw lỗi hay không
  10. XCTAssertThrowsError() dùng khi cần kiểm tra func có throw error và kiểm tra được error

Để các bạn dễ hình dung hơn mình sẽ đưa ra một ví dụ như sau:

Tài liệu yêu cầu bạn phải viết một hàm để tính chu vi của hình chữ nhật khi biết chiều dài và chiều rộng của nó. Ta biết chu vi của hình chữ nhật chính là tổng tất cả các cạnh của nó, vì vậy ta có thể sẽ viết func như sau:

class Regtangle {
    // hàm tính chu vi hình chữ nhật
    class func perimeter(width: Int, height: Int) -> Int {
       (width + height) * 2
    }
}

Để viết bắt đầu viết test case chúng ta sẽ cần tạo file test như sau:

Chuột phải vào thư mục cần tạo file -> chọn new file -> Unit Test Case Class -> Next

Đặt tên cho file test, trong trường hợp này mình đang cần test class Regtangle nên mình đặt tên như hình -> Next

Sau khi tạo file chúng ta XCode sẽ tạo sẵn cho chúng ta một số đoạn code cơ bản như sau:

import XCTest
// thêm target cần test
@testable import UTXcode14

final class RegtangleTests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
        // Any test you write for XCTest can be annotated as throws and async.
        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
    }

    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }

}

func testPerformanceExample() đây là hàm để kiểm tra hiệu suất của đoạn code, nếu bạn không cần kiểm tra thì bỏ nó đi giúp mỗi lần chạy test của bạn sẽ nhanh hơn đáng kể.

Bây giờ chúng ta đã có thể viết test case để kiêm tra class Regtangle. Đối với func tính chu vi như vậy thì ta sẽ cần dựa vào yêu cầu và phân tích tích bài toán một chút.

Chúng ta hiểu rằng chiều cao và chiều rộng của hình chữ nhật phải là số lớn hơn 0, chu vi của hình chữ nhật thì bằng tổng chiều dài bốn cạnh, vậy nên chúng ta sẽ cần viết các test case với trường hợp như sau:

  1. width và height đều lớn hơn 0: Đầu ra là (width + height) * 2
  2. width <= 0, height > 0: đầu ra cần phải là một thông báo lỗi
  3. width > 0, height <= 0: Đầu ra cần phải là một thông báo lỗi
  4. width <= 0, height <= 0: Đầu ra cần phải là một thông báo lỗi

Đối với case số 1 yêu cầu dữ liệu đầu vào cả width và height đều phải là một số > 0 thì ta viết như sau:

   // case width > 0 and height > 0
   func test_perimeter_case1() {
        // input
        let width: Int = 3
        let height: Int = 2
        // expectation
        let expectation = 10
        // run code
        let result = Regtangle.perimeter(width: width, height: height)
        
        // verify
        XCTAssertEqual(result, expectation)
    }

Chạy test ta thu đươc kết quả như hình, dấu tích xanh thể hiện kết quả với expectation là bằng nhau, có nghĩa là trong trường hợp này hàm Regtangle.perimeter() đã chạy đúng.

Tiếp theo chúng ta sẽ viết tiếp test case số 2, width <=0 và height > 0, đây là trường hợp chiều rộng nhỏ hơn 0 vì vậy nó là một trường hợp lỗi, mình sẽ mong đợi một thông báo lỗi “Width must be greater than zero”. Vậy nên mình viết code như hình dưới

Lúc này Xcode sẽ báo lỗi như hình trên là do chúng ta đang so sánh 2 kiểu dữ liệu khác nhau. Quay lại hàm tính chu vi hình chữ nhật thì chúng ta thấy không có logic kiểm tra width và height điều này khiến cho func này không đảm bảo tính đúng đắn của nó.

Vậy viết func tính chu vi như nào mới đúng? các bạn có thể tham khảo một số cách viết của mình như sau:

Cách 1: Sử dụng Result để trả về kết quả

// equatable để tiện cho việc so sánh khi viết Unit test
struct MyError: Error, Equatable {
    let message: String
}

class Regtangle {
    // hàm tính chu vi hình chữ nhật
    class func perimeter(width: Int, height: Int) -> Result<Int, MyError> {
        if width <= 0 && height <= 0 {
            return .failure(MyError(message: "Width and height must be greater than zero"))
        } else if width <= 0 {
            return .failure(MyError(message: "Width must be greater than zero"))
        } else if height <= 0 {
            return .failure(MyError(message: "Height must be greater than zero"))
        } else {
            let perimeter = (width + height) * 2
            return .success(perimeter)
        }
    }
}

Cách 2: Sử dụng throw để đẩy ra lỗi

struct MyError: Error, Equatable {
    let message: String
}

class Regtangle {
    // hàm tính chu vi hình chữ nhật
    class func perimeter(width: Int, height: Int) throws -> Int {
        if width <= 0 && height <= 0 {
            throw MyError(message: "Width and height must be greater than zero")
        } else if width <= 0 {
            throw MyError(message: "Width must be greater than zero")
        } else if height <= 0 {
            throw MyError(message: "Height must be greater than zero")
        } else {
            return (width + height) * 2
        }
    }
}

Trong bài viết này mình sẽ hướng dẫn các bạn viết test case khi sử dụng throw, mình sẽ viết tổng cộng 7 test cases để thực hiện test func này, trong đó bao gồm 4 test cases để test logic chính và 3 cases để test giá trị biên. Cụ thể mình sẽ thực hiện như sau:

import XCTest
@testable import UTXcode14

final class RegtangleTests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    // case width > 0 and height > 0
    func test_perimeter_case1() throws {
        // precontidtion
        let width: Int = 3
        let height: Int = 2
        // expectation
        let expectation = 10
        // run code
        XCTAssertNoThrow(try Regtangle.perimeter(width: width, height: height))
        let result = try Regtangle.perimeter(width: width, height: height)
        
        // verify
        XCTAssertEqual(result, expectation)
    }
    
    // case width < 0 and height > 0
    func test_perimeter_case2() throws {
        // precontidtion
        let width: Int = -3
        let height: Int = 2
        // expectation
        let expectation = MyError(message: "Width must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
    
    // case width > 0 and height < 0
    func test_perimeter_case3() throws {
        // precontidtion
        let width: Int = 3
        let height: Int = -2
        // expectation
        let expectation = MyError(message: "Height must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
    
    // case width < 0 and height < 0
    func test_perimeter_case4() throws {
        // precontidtion
        let width: Int = -3
        let height: Int = -2
        // expectation
        let expectation = MyError(message: "Width and height must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
    
    // case width = 0 and height > 0, test giá trị biên của width
    func test_perimeter_case5() throws {
        // precontidtion
        let width: Int = 0
        let height: Int = 2
        // expectation
        let expectation = MyError(message: "Width must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
    
    // case width = 0 and height > 0, test giá trị biên của height
    func test_perimeter_case6() throws {
        // precontidtion
        let width: Int = 2
        let height: Int = 0
        // expectation
        let expectation = MyError(message: "Height must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
    
    // case width = 0 and height = 0, test giá trị biên của cả width và height
    func test_perimeter_case7() throws {
        // precontidtion
        let width: Int = 0
        let height: Int = 0
        // expectation
        let expectation = MyError(message: "Width and height must be greater than zero")
        // run code
        XCTAssertThrowsError(try Regtangle.perimeter(width: width, height: height)) { error in
            XCTAssertEqual(error as? MyError, expectation)
        }
    }
}

Chạy test (Command U) chúng ta được kết quả như sau:

Tất cả các test case của chúng ta đều passed, điều này chứng minh func của bạn đã đáp ứng hết tất cả yêu cầu mà bạn đặt ra

Hi vọng bài viết sẽ giúp cho các bạn có thêm kiến thức để nâng cao năng lực của bản thân.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

You may also like