swift

Viết unit test tự động download và mapping JSON từ network với Swift

Giới thiệu

Như chúng ta đã biết, JSON được sử dụng cực kỳ phổ biến trong các app mobile hiện nay.

JSON được dùng để lưu dữ liệu dưới local, dùng để download thông tin từ trên mạng hay để chứa các config... Đâu đâu cũng thấy JSON.

Chính vì dùng JSON rất nhiều như vậy nên những lỗi xảy ra trong quá trình xử lý JSON (đặc biệt là khi mapping JSON với các class data model) sẽ khiến app bị crash hoặc không hoạt động đúng.

Vậy nên việc viết các test để đảm bảo những lỗi không mong muốn không xảy ra là cực kỳ quan trọng. Và chúng ta cần test sao cho cover nhiều case nhất có thể.

Trong bài viết này, chúng ta sẽ tìm hiểu về cách viết các test để test việc mapping JSON một cách toàn diện nhất có thể với Swift.

Vấn đề

Thông thường, khi nói đến việc viết test, hầu như chúng ta sẽ nghĩ ngay đến unit test. Unit test là các test giúp verify một unit trong app (function, module...) hoạt động đúng một cách độc lập.

Việc unit test đem lại nhiều giá trị, giúp chúng ta có thể tích hợp các module nhỏ của ứng dụng một cách dễ dàng mà không cần viết lại toàn bộ phần test mỗi khi có một unit thay đổi. Điều này nâng cao chất lượng của toàn bộ app.

Tuy nhiên, khi test mapping JSON, chúng ta sẽ phải đối mặt với một chút khó khăn.

Đó là viết unit test khi khởi tạo các object data model class từ JSON, chúng ta thường chỉ test được một vài case nhất định của file JSON cần mapping mà không thể cover nhiều case hơn như trong khi app chạy trong môi trường thật, với API thật. Hãy xem ví dụ sau.

Giả sử chúng ta có một data model struct User như sau:

struct User {
    let name: String
    let age: Int
}

Bạn có thể sử dụng bất kỳ library nào tùy thích. Ví dụ này sẽ dùng Himotoki để decode JSON thành object.

import Himotoki

extension User: Himotoki.Decodable {

    static func decode(_ e: Extractor) throws -> User {
        return try User(
            name: e <| "name",
            age: e <| "age"
        )
    }
    
}

Và cần mapping từ file JSON này:

{
    "name": "John",
    "age": 29
}

Bây giờ hãy viết một test case để verify model class User có thể khởi tạo được từ đoạn JSON trên.

Cụ thể, ta cần thêm file JSON trên vào bundle hiện tại, load vào test case và cuối cùng verify xem instance User có thể khởi tạo được không và compare các property của nó.

class UserTests: XCTestCase {

    func testJSONMapping() throws {
        let bundle = Bundle(for: type(of: self))

        guard let url = bundle.url(forResource: "User", withExtension: "json") else {
            XCTFail("Missing file: User.json")
            return
        }

        let json = try Data(contentsOf: url)
        let user = try User.decodeValue(json)

        XCTAssertEqual(user.name, "John")
        XCTAssertEqual(user.age, 29)
    }
    
}

Đây chỉ là một ví dụ cơ bản nhưng hãy thử tưởng tượng điều gì sẽ xảy ra nếu nội dung JSON mà client nhận được từ server API thay đổi.

Vấn đề là nếu chỉ viết test case đơn giản như trên thì sẽ không thể cover được trường hợp lỗi xảy ra như kể trên.

File JSON cố định trên sẽ luôn pass được test case và chúng ta sẽ nghĩ mọi chuyện đều ổn.

Nhưng một ngày đẹp trời nào đó, server backend thay đổi, chỉ cần sai một property thôi và thế là 💥, client app sẽ không hoạt động đúng, thậm chí là crash.

Viết code test download JSON tự động

Để có thể hạn chế lỗi khi backend thay đổi, chúng ta cần một giải pháp test mapping JSON toàn diện hơn.

Đó là cần phải cover tất cả các trường hợp có thể xảy ra của file JSON và phải đảm bảo dữ liệu được truyền từ server trả về phải được mapping đúng với các model class.

Thông thường, chúng ta sử dụng UI Testing để có thể kiểm thử một cách toàn diện nhất nhưng trong trường hợp này sẽ hơi không cần thiết.

Viết UI Test cho sẽ mất khá nhiều công sức vì không cần phải chạy cả test UI chỉ để test mapping JSON. Và cũng khó mà có thể verify tính đúng đắn của mapping JSON trong UI Test.

Tuy nhiên, có một cách test giúp giải quyết bài toán trên.

Đó là viết code để tạo request, lấy data trực tiếp từ backend về để test.

Tuy nhiên chúng ta sẽ không request API liên tục mà chỉ cần thực hiện việc này trong một khoảng thời gian định kỳ, ví dụ một ngày thực hiện test một lần chẳng hạn.

Hãy bắt đầu với lib Marathon, một command line tool giúp viết và run code Swift một cách đơn giản được hỗ trợ bởi Swift Package Manager.

Bây giờ hãy viết code Swift để download một chút dữ liệu từ API search của Github mỗi ngày.

Nếu bạn đã cài thành công Marathon, đơn giản chỉ cần chạy câu lệnh sau:

$ marathon create DownloadJSON

Câu lệnh trên sẽ tạo ra một file tên là DownloadJSON.swift. Mở file này lên bằng Xcode hoặc bất kỳ IDE nào bạn muốn và code tiếp.

import Foundation
import Files

// Lưu tất cả các file JSON download được vào folder 'Scripts'
let scriptsFolder = try Folder.current.subfolder(named: "Scripts")
let currentTimestamp = Date().timeIntervalSinceReferenceDate

// Dựa vào file .lastRun và giá trị timestamp của lần download trước để đảm bảo chỉ download file JSON một lần một ngày
if let lastRunFile = try? scriptsFolder.file(named: ".lastRun") {
    let lastRunTimestamp = try TimeInterval(lastRunFile.readAsInt())
    let oneDayInterval: TimeInterval = 60 * 60 * 24
    let timestampDelta = currentTimestamp - lastRunTimestamp

    // Nếu chưa đủ một ngày exit code
    if timestampDelta < oneDayInterval {
        exit(0)
    }
}

// Download JSON
let query = "language:swift"
let url = URL(string: "https://api.github.com/search/repositories?q=\(query)")!
let json = try Data(contentsOf: url)

// Ghi JSON file
let resourceFolder = try Folder.current.subfolder(atPath: "Tests/Resources")
try resourceFolder.createFile(named: "GitHubSearch.json", contents: json)

// Tạo mới file .lastRun và giá trị timestamp của lần download cuối
let lastRunFile = try scriptsFolder.createFile(named: ".lastRun")
try lastRunFile.write(string: String(Int(currentTimestamp)))

Để đoạn code Swift trên được chạy cùng một lần run test, hãy thêm mới một Run script vào test target hiện tại.

  • Click vào project hiện tại trong Xcode's project navigator, chọn test target cần thêm.
  • Mở tab Build phases và nhấn vào nút '+' .
  • Thêm run script vào đầu tiên, ngay sau Target Dependencies.
  • Đặt tên là "Download JSON" với đoạn script sau:
marathon run Scripts/DownloadJSON

Tiếp theo, run test target và bạn sẽ thấy một file GitHubSearch.json bên trong folder tên TestResources xuất hiện ở thư mục gốc của project hiện tại.

Kéo thả thư mục vừa tạo và add vào target test của bạn. Và thế là chúng ta đã có file JSON được auto-update mỗi ngày từ server API thật, được dùng làm input cho unit test dưới đây.

File data model Repository.swift

import Himotoki

struct Repository {
    let id: Int
    let name: String
}

extension Repository: Himotoki.Decodable {

    static func decode(_ e: Extractor) throws -> Repository {
        return try Repository(
            id: e <| "id",
            name: e <| "name"
        )
    }
    
}

Và file unit test tương ứng để test mapping JSON.

class RepositoryTests: XCTestCase {

    func testJSONMapping() throws {
        let bundle = Bundle(for: type(of: self))

        guard let url = bundle.url(forResource: "GitHubSearch", withExtension: "json") else {
            XCTFail("Missing file: GitHubSearch.json")
            return
        }

        let json = try Data(contentsOf: url)
        let repositories = try [Repository].decode(json, rootKeyPath: "items")

        // Chúng ta có thể thêm bất kỳ test logic nào để verify mapping JSON ở đây
        
        XCTAssertFalse(repositories.isEmpty)
    }
    
}

Vậy là chỉ với vài bước cơ bản, chúng ta đã có thể viết unit test cho việc mapping JSON không phải từ những file JSON cố định dưới local mà có thể tự động download trực tiếp dữ liệu từ server backend mỗi lần một ngày khi run test.

Kết luận

Cả unit test thông thường và unit test tự động như ở trên đều mang lại những giá trị nhất định.

Unit test thông thường giúp chúng ta cover các case cố định biết trước.

Trong khi unit test tự động download và mapping lại giúp chúng ta test và phòng ngừa các trường hợp lỗi xảy ra do sự thay đổi dữ liệu từ server backend thật trả về.

Vì vậy kết hợp sử dụng cả 2 loại sẽ là tốt nhất cho việc test.

Source article: https://www.swiftbysundell.com/posts/writing-end-to-end-json-mapping-tests-in-swift

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration