Bắt đầu phát triển iOS Apps với Swift part 5: Định nghĩa Data Model và viết Unit Test

Đây là phần 5 trong series Bắt đầu phát triển iOS Apps với Swift Các phần trước các bạn có thể xem ở đây nhé. part 1: Xây dựng Basic UI part 2: Kết nối UI và Source Code part 3: Làm việc với View Controller part4: Tự tạo một Custom Control

Trong phần 5 này chúng ta sẽ cùng nhau định nghĩa và test data model cho app FoodTracker. data model là model thể hiện cấu trúc của thông tin được lưu trong app. Chúng ta sẽ hoc được những kiến thức sau khi hoàn thiện các bước thực hành của bài ngày hôm nay.

  1. Tạo được một data model
  2. Viết một hàm khởi tạo có thể bị thất bại
  3. Chứng minh và hiểu sự khác biệt của hàm khởi tạo có thể thất bại và hàm khởi tạo đảm bảo không có lỗi.
  4. Test data model bằng cách viết và chạy Unit Tests

Nào, chúng ta cùng nhau bắt đầu

I. Tạo Data Model

Hiện tại chúng ta đang muốn tạo data model để lưu thông tin của các món ăn trong màn hình list các món ăn. Để làm điều này chúng ta tạo một class đơn giản với các thông tin như tên, ảnh và giá trị đánh giá món ăn. Để tạo một data model class

  1. Chọn File > New > File hoặc nhấn tổ hợp phím Command-N
  2. Chọn iOS ở trên dialog vừa xuất hiện.
  3. Chọn Swift File và click Next Chúng ta đang tạo một class mới không theo cách trước đó chúng ta đã tạo RatingControl (iOS > Source > Cocoa Touch Class). Lý do bởi vì chúng ta đang tạo base class cho data model, nghĩa là nó không cần thiết phải kế thừa từ bất kì class nào khác.
  4. Ở trường Save As, gõ Meal
  5. Lưu file được tạo default trong folder của project. Ở Group option để default tên app, FoodTracker Ở Targets section, app được chọn vào test your app không để tick.
  6. Để các mục con lại với giá trị default của nó và click Create

Định nghĩa data model cho món ăn

  1. Chuyển sang chế độ Standard Editor, mở Meal.swift
  2. Chuyển import Foundation thành import UIKit
import UIKit

Xcode default khai báo Foundation framework để giúp chúng ta sử dụng được cấu trúc dữ liệu định nghĩa bởi Foundation. Tuy nhiên chúng ta sẽ cần làm việc với class từ UIKit framework nữa, mà khi import UIKit rồi thì chúng ta có thể truy cập vào Foundation framework một cách bình thường nên chúng ta xoá đi phần import Foundation thừa đi. 3. Thêm đoạn code khai báo properties của các món ăn

class Meal {
    
    //MARK: Properties
    
    var name: String
    var photo: UIImage?
    var rating: Int
    
}
  1. Định nghĩa hàm khởi tạo
//MARK: Initialization
 
init(name: String, photo: UIImage?, rating: Int) {
    
}
  1. Set các giá trị khởi tạo bằng các parameter được truyền vào.
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating

Nhưng điều gì sẽ xảy ra nếu các name bị trống hay rating có giá trị là số âm? Khi đó chúng ta không thể khởi tạo được dữ liệu cho món ăn. Các bạn phải trả về giá trị nil để chỉ ra rằng không có item nào được tạo ra. 6. Thêm đoạn code này trước đoạn set giá trị cho các properties.

// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0  {
    return nil
}
  1. Click vào icon thông báo lỗi để fix
  2. Click đúp vào để sửa hàm khởi tạo. Hàm khởi tạo đúng sẽ có dạng dưới đây:
init?(name: String, photo: UIImage?, rating: Int) {

Hàm khởi tạo có thể thất bại bắt buộc phải bắt đầu bằng init? hoặc init!. Hàm này trả về giá trị Optional hay giá trị implicitly unwrapped optional . Optionals có thể là giá trị có nội dung hoặc nil. Vì vậy bạn phải kiểm tra giá trị của chúng hoặc unwrap cẩn thận trước khi sử dụng. Trong trường hợp này thì hàm khởi tạo của chúng ta sẽ trả về một optional object Meal? Hàm khởi tạo bây giờ của chúng ta có dạng:

init?(name: String, photo: UIImage?, rating: Int) {
    
    // Initialization should fail if there is no name or if the rating is negative.
    if name.isEmpty || rating < 0  {
        return nil
    }
    
    // Initialize stored properties.
    self.name = name
    self.photo = photo
    self.rating = rating
    
}

II. Test Data Model

Đến thời điểm này mặc dù bạn đã code xong, nhưng chúng ta chưa tích hợp model này vào trong app một cách hoàn toàn. Chúng ta chưa thể biết nó hoạt động đúng hay sai; những giá trị ngoại lệ chúng ta muốn control liệu đã đủ hay chưa. Nếu để tới lúc tích hợp hoàn toàn vào để chạy thì sẽ rất lâu, hơn nữa chạy máy ảo cũng rất tốn tài nguyên máy và thời gian. Thật may chúng ta có giải pháp cho vấn đề này. Đó là viết Unit Test để test những phần code nhỏ, hoàn chỉnh về mặt chức năng để đảm bảo các chức năng của nó hoạt động đúng như mong đợi. Xcode đã tạo sẵn unit test file cho chúng ta rồi. Cùng điểm qua nội dung của file đó

  1. Mở FoodTrackerTests folder và chọn FoodTrackerTests.swift file
  2. Mở FoodTrackerTests.swift
import XCTest
@testable import FoodTracker
 
class FoodTrackerTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
    
}

Một vài điểm cần vọc từ file này như: @testable: Attribute này cho phép tests có thể truy cập vào các internal elements của code trong app. XCTest: Viết tắt của Xcode’s testing framework

Test cases đơn giản là các method mà hệ thống sẽ tự động chạy như là một phần của Unit Tests các bạn viết. Để tạo test case bạn tạo method với prefix là test. Cách viết unit test cho hàm khởi tạo Meal

  1. Xoá tất cả các template method sẵn có ở FoodTrackerTests.swift đi. Chúng ta chỉ để lại các khác báo cơ bản.
import XCTest
@testable import FoodTracker
 
class FoodTrackerTests: XCTestCase {
    
}
  1. Thêm MARK
//MARK: Meal Class Tests
  1. Thêm một test case mới.
// Confirm that the Meal initializer returns a Meal object when passed valid parameters.
func testMealInitializationSucceeds() {
    
}

Hệ thống sẽ tự động chạy test case khi mà unit tests được chạy. 4. Thêm nội dung test để kiểm tra trường hợp rating 0 và rating max 5

// Zero rating
let zeroRatingMeal = Meal.init(name: "Zero", photo: nil, rating: 0)
XCTAssertNotNil(zeroRatingMeal)
 
// Highest positive rating
let positiveRatingMeal = Meal.init(name: "Positive", photo: nil, rating: 5)
XCTAssertNotNil(positiveRatingMeal)

Nếu hàm khởi tạo hoạt động đúng, thì với các giá trị truyền vào như test ta sẽ có các object Meal không bị nil 5. Tiếp theo đó chúng ta viết test case khi mà hàm khởi tạo bị fail.

// Confirm that the Meal initialier returns nil when passed a negative rating or an empty name.
func testMealInitializationFails() {
    
}
  1. Thêm vào các trường hợp mà tham số sai.
// Negative rating
let negativeRatingMeal = Meal.init(name: "Negative", photo: nil, rating: -1)
XCTAssertNil(negativeRatingMeal)
 
// Empty String
let emptyStringMeal = Meal.init(name: "", photo: nil, rating: 0)
XCTAssertNil(emptyStringMeal)

Nếu hàm khởi tạo hoạt động đúng chúng ta sẽ thấy nó trả về nil. 7. Tiếp theo chúng ta sẽ thêm vào một đoạn code mà test thất bại với giá trị rating là 6.

// Rating exceeds maximum
let largeRatingMeal = Meal.init(name: "Large", photo: nil, rating: 6)
XCTAssertNil(largeRatingMeal)

Kiểm tra: Chạy Unit Test bằng cách chọn Product > Test hoặc tổ hợp phím Command-U Kết quả testMealInitializationFails() thất bại.

Cùng sửa lỗi đó nào

  1. Vào file Meal.swift, tìm hàm init?
  2. Thay đoạn code
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0  {
    return nil
}

Bằng

// The name must not be empty
guard !name.isEmpty else {
    return nil
}
 
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
    return nil
}

Đoạn code này đảm bảo cho rating nằm trong khoảng cho phép [0;5] Hàm khởi tạo của chúng ta bây giờ sẽ có dạng

init?(name: String, photo: UIImage?, rating: Int) {
    
    // The name must not be empty
    guard !name.isEmpty else {
        return nil
    }
    
    // The rating must be between 0 and 5 inclusively
    guard (rating >= 0) && (rating <= 5) else {
        return nil
    }
    
    // Initialize stored properties.
    self.name = name
    self.photo = photo
    self.rating = rating
    
}

Kiểm tra: Đến đây ta chạy lại test và kiểm tra kết quả Tất cả các test đã pass!