Skip to content

Test Model

cuonghx-0882 edited this page Nov 25, 2019 · 2 revisions

Test Model

Bạn là 1 nhà phát triển có lẽ bạn cũng biết qua hoặc đã từng sử dụng MVC, MVVM,.. Chúng đều có một thành phần là Model. Model chịu trách nhiệm lưu trữ dữ liệu, tương tác hai chiều (Read & Update) trực tiếp với ViewModel (MVVM), Controller(MVC). Vậy làm sao để đảm đảm bảo được một Model là đúng chúng ta sẽ cùng bắt đầu tìm hiểu nhé.

Sử dụng XCTAssert để Test Models

XCTAssert là một trong những nhóm quen thuộc để tạo xác nhận cho Unit Test từ framework XCTest và chỉ có trong Unit Test Targets. Nếu một XCTAssert thất bại nó sẽ ghi lại và báo cáo lỗi. Nhìn chung XCTAssert giúp chúng ta khẳng định tính đúng đắn của biểu thức. Chi tiết vể XCTest đã được đề cập rất chi tiết trong phần XCTest-Wiki.

Để hiểu hơn chúng ta sẽ đến với một ví dụ đơn giản tạo một đối tượng Person sử dụng ObjectMapper handler đối tượng từ json và Test Model giúp chúng ta như nào trong trường hợp này.

import Foundation
import ObjectMapper

struct Person {
    var name = ""
    var address = ""
    var age = 0
}

extension Person: Mappable {
    
    init?(map: Map) { self.init() }
    
    mutating func mapping(map: Map) {
        name <- map["name"]
        address <- map["address"]
        age <- map["age"]
    }
}

Chúng ta sẽ tạo ModelPersonTests đảm bảo việc khởi tạo Person đúng với dữ liệu từ json trả về.

import XCTest
@testable import TestDemo

class ModelPersonTests: XCTestCase {
    
    func testInitModel() {
        let json: [String: Any] = [
            "name": "foo",
            "address": "bar",
            "age": 19
        ]
        
        let person = Person(JSON: json)
        XCTAssertNotNil(person)
        XCTAssertEqual(person?.name, "foo")
        XCTAssertEqual(person?.address, "bar")
        XCTAssertEqual(person?.age, 19)
    }
    
}

Test Driven Model

Xây dựng Model

Vậy làm sao để xây dựng một Model theo kỹ thuật sử dụng TDD. Giả định chúng ta sẽ xây dựng Class AccountOwner đại diện cho một cá nhân sở hữu một tài khoản với các thuộc tính:

  • FirstName
  • LastName
  • Email

Tạo một lớp Unit Test Case mới có tên AccountOwnerTest với method Unit Test testAccountOwner_ValidFirstName_ ValidLastName_ValidEmail_CanBeInstantiated () và thêm mã sau vào method:

func testAccountOwner_ValidFirstName_ValidLastName_ValidEmail_ CanBeInstantiated() { 
   let accountOwner = AccountOwner(firstName: validFirstName, lastName: validLastName, emailAddress: validEmailAddress) 
   XCTAssertNotNil(accountOwner) 
}

Tiếp theo hãy thêm các khai báo biến sau vào đầu file AccountOwnerTests.swift

private let validFirstName = "Andrew" 
private let validLastName = "Hill" 
private let validEmailAddress = "a.hill@abcfinancial.com" 
private let invalidFirstName = "A" 
private let invalidLastName = "h" 
private let invalidEmailAddress = "abcfinancial.com" 
private let emptyString = ""

Các biến này đại diện cho một tập các tên, họ, email hợp lệ và không hợp lệ sẽ sử dụng trong các trường hợp để thực hiện Test case cho lớp này. Tiếp đến chúng ta cần thực hiện khởi tạo lớp AccountOwner

Chúng ta cần thêm một vài test case nữa để đảm bảo tính đúng cho class AccountOwner:

    func testAccountOwner_InvalidFirstName_ValidLastName_ValidEmail_CanNotBeInstantiated() {
        let accountOwner = AccountOwner(firstName: invalidFirstName,
                                        lastName: validLastName,
                                        emailAddress: validEmailAddress)
        XCTAssertNil(accountOwner)
    }
    
    func testAccountOwner_InvalidFirstName_InvalidLastName_ValidEmail_CanNotBeInstantiated() {
        let accountOwner = AccountOwner(firstName: invalidFirstName,
                                        lastName: invalidLastName,
                                        emailAddress: validEmailAddress)
        XCTAssertNil(accountOwner)
    }

Và tương tự với các case còn lại. Với 3 thuộc tính chúng ta sẽ có chín trường hợp cần kiểm tra để đảm bảo chung rằng một đối tượng AccountOwner không thể được khởi tạo nếu tên, họ hoặc địa chỉ email không hợp lệ.

Xây dựng lớp Validator

Chúng ta sẽ giả sử yêu cầu cho thuộc tính name:

  • Nên có độ dài từ 2 đến 10 ký tự.
  • Không nên bao gồm khoảng trắng. Trước tiên chúng ta sẽ tạo một tập kiểm tra cho FirstNameValidator, Tạo class FirstNameValidatorTests nhằm đảm bảo cho Validator
class FirstNameValidatorTests: XCTestCase {
    fileprivate let emptyString = ""
    fileprivate let singleCharachterName = "a"
    fileprivate let twoCharachterName = "ab"
    fileprivate let tenCharachterName = "abcdefghij"
    fileprivate let elevenCharachterName = "abcdefghijk"
    fileprivate let nameWithWhitespace = "abc def"
}

// MARK: Empty string validation
extension FirstNameValidatorTests {
    func testValidate_EmptyString_ReturnsFalse() {
        let validator = FirstNameValidator()
        XCTAssertFalse(validator.validate(emptyString), "string can not be empty.")
    }
}

// MARK: String length validation
extension FirstNameValidatorTests {
    func testValidate_InputLessThanTwoCharachtersInLength_ReturnsFalse() {
        let validator = FirstNameValidator()
        XCTAssertFalse(validator.validate(singleCharachterName), "string can not have less than 2 characters.")
    }
    func testValidate_InputGreaterThanTenCharachtersInLength_ReturnsFalse() {
        let validator = FirstNameValidator()
        XCTAssertFalse(validator.validate(elevenCharachterName), "string can not have more than 11 characters.")
    }
    func testValidate_InputTwoCharachtersInLength_ReturnsTrue() {
        let validator = FirstNameValidator()
        XCTAssertTrue(validator.validate(twoCharachterName), "string with 2 charachters should have been valid.")
    }
    func testValidate_InputTenCharachtersInLength_ReturnsTrue() {
        let validator = FirstNameValidator()
        XCTAssertTrue(validator.validate(tenCharachterName), "string with 10 charachters should have been valid.")
    }
}

// MARK: white space validation
extension FirstNameValidatorTests {
    func testValidate_InputWithWhitespace_ReturnsFalse() {
        let validator = FirstNameValidator()
        XCTAssertFalse(validator.validate(nameWithWhitespace), "string can not have white space.")
    }
}

Test case giả định lớp FirstNameValidator có method là validate() trả về true hoặc false tương ứng. Nhưng chúng ta vẫn chưa có Class FirstNameValidator nào cả. Tiếp đến chúng ta cần phải tạo

import Foundation
class FirstNameValidator: NSObject {
    func validate(_ value:String) -> Bool {
        let validated: Bool = false
        // Do somthing here to validate
        return validated 
    }
}

Vậy là chúng ta đã tạo lớp Model bằng các kỹ thuật Test-Driven. Điểm mấu chốt của phương pháp này là trước tiên bạn tạo một tập hợp các trường hợp thử nghiệm, sau đó xây dựng lớp model tương ứng để đảm bảo các test case . Bạn cũng đã học cách tạo các đối tượng Validator nội dung của các đối tượng lớp mô hình. Với phương pháp này ngoài ra bạn còn có thể giả định Lớp Validator bằng phương pháp Mock.

Testing Core Data

Các đối tượng được xây dựng mà đa phần chúng ta sử dụng đều là các lớp con NSObject. Tuy nhiên có một số ứng dụng đang sử dụng các framework lưu trữ đối tượng như Core Data. Để test lớp Model có lẽ là một trong những rào cản lớn nhất phải đối mặt khi lớp Model sử dụng Core Data.

Nếu bạn chưa hiểu rõ thì có thể tưởng tượng giả định rằng một test case ghi một số dữ liệu vào database và một test case khác đọc một số dữ liệu. Việc thực hiện như vậy sẽ tạo ra mối liên kết giữa các thử nghiệm, thứ tự thực hiện của các test case trở nên quan trọng và các test case không còn độc lập với nhau. Tồi tệ hơn nữa là khi loại hành vi này vô tình xảy ra mà chúng ta không biết được.

Vậy giải pháp ở đây là gì, Core Data có một tính năng thường bị chúng ta bỏ qua đó là: "lưu trữ trong bộ nhớ tạm" trên RAM. Việc lưu trữ trong bộ nhớ tạm dữ liệu có thể bị xoá bỏ và được tạo lại ở trạng thái ban đầu mà không làm tiêu tốn performance. Đoạn mã sau đây cho thấy cách bạn có thể tạo managed object context sử dụng lưu trữ trong bộ nhớ tạm:

func inMemoryManagedObjectContext() -> NSManagedObjectContext? {
    guard let managedObjectModel = NSManagedObjectModel.
    mergedModel(from:[Bundle.main]) else {
        return nil
    }
    let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObj
        ectModel: managedObjectModel)
    do {
        try persistentStoreCoordinator.addPersistentStore(ofType:
            NSInMemoryStoreType,
                                                          configurationName: nil,
                                                          at: nil,
                                                          options: nil)
    } catch {
        print("Failed to create in-memory persistent store.")
        return nil
    }
    let managedObjectContext = NSManagedObjectContext(concurrencyType:
        .mainQueueConcurrencyType)
    managedObjectContext.persistentStoreCoordinator =
    persistentStoreCoordinator
    return managedObjectContext
}