[Lofi Time Radio App] TDD + Network Module (Phần 1)

Dec 23 2022
Chào bạn! Trong bài đăng này, tôi sẽ nói về TDD và phần đầu tiên trong ứng dụng của tôi “Mô-đun mạng”. TDD là gì? Phát triển theo hướng thử nghiệm là một phương pháp phát triển phần mềm, về cơ bản tuyên bố rằng đối với mỗi đơn vị phần mềm, nhà phát triển phần mềm phải: Xác định một bộ kiểm tra cho đơn vị đó trước tiên Làm cho mã hoạt động được và thử nghiệm thất bại Sau đó triển khai đơn vị Cuối cùng xác minh rằng việc triển khai đơn vị làm cho các bài kiểm tra thành công.

Chào bạn! Trong bài đăng này, tôi sẽ nói về TDD và phần đầu tiên trong ứng dụng của tôi “Mô-đun mạng”.

TDD là gì?

Phát triển theo hướng thử nghiệm là một phương pháp phát triển phần mềm về cơ bản nói rằng đối với mỗi đơn vị phần mềm, nhà phát triển phần mềm phải:

Xác định một bộ kiểm tra cho đơn vị đầu tiên

Làm cho mã hoạt động, kiểm tra thất bại

Sau đó thực hiện các đơn vị

Cuối cùng xác minh rằng việc thực hiện đơn vị làm cho các bài kiểm tra thành công.

Đã đến lúc bắt đầu viết mã!!!

Xác định một bộ kiểm tra cho đơn vị đầu tiên

final class RemoteStationLoaderTests: XCTestCase {

    func test_requestURLs() {
        
        let client = HTTPClient()
        let loader = RemoteStationLoader(client: client)
        
        let url = URL(string: "https://any-url.com")!
        loader.load(url: url)
        
        XCTAssertEqual(client.requestURLs, [url])
        
    }
    
}

class RemoteStationLoader {
    private let client: HTTPClient
    
    init(client: HTTPClient) {
        self.client = client
    }
    
    func load(url: URL) {
        
    }
}

final class HTTPClient {
    var requestURLs = [URL]()
    
    func load(url: URL) {
        
    }
}

final class RemoteStationLoaderTests: XCTestCase {

    func test_requestURLs() {
        
        let client = HTTPClient()
        let loader = RemoteStationLoader(client: client)
        
        let url = URL(string: "https://any-url.com")!
        loader.load(url: url)
        
        XCTAssertEqual(client.requestURLs, [url])
        
    }

}

Cuối cùng xác minh rằng việc thực hiện đơn vị làm cho các bài kiểm tra thành công.

class RemoteStationLoader {
    private let client: HTTPClient
    
    init(client: HTTPClient) {
        self.client = client
    }
    
    func load(url: URL) {
        client.load(url: url)
    }
}

final class HTTPClient {
    var requestURLs = [URL]()
    
    func load(url: URL) {
        requestURLs.append(url)
    }
}

final class RemoteStationLoaderTests: XCTestCase {

    func test_requestURLs() {
        
        let client = HTTPClient()
        let loader = RemoteStationLoader(client: client)
        
        let url = URL(string: "https://any-url.com")!
        loader.load(url: url)
        
        XCTAssertEqual(client.requestURLs, [url])
        
    }

}

Như bạn cấu trúc app ở trên, mình sẽ tạo framework hỗ trợ mọi nền tảng (iOS và macOS) có tên là LofiTimeRadioStation.

API trạm (Mô-đun mạng)

Như mình trước khi học CHƯƠNG TRÌNH iOS LEAD ESSENTIALS , trong project cá nhân mình thường tạo 1 class singleton tên là Service

public final class StationLoader {
  public let shared = StationLoader()

  private init() {}

  func load(url: URL) {
    // Code 
  }
}

Đảm bảo họ chỉ có một ví dụ

Tôi làm cho lớp có thể có nhiều hơn một phiên bản để tôi có thể tạo spyđể tránh gọi dịch vụ thực trong bài kiểm tra đơn vị

public final class StationLoader {
  public var shared = StationLoader()
  ...
}

public protocol HTTPClient {
  func load(url: URL)
}

public final class StationLoader {
  private let client: HTTPClient

  public init(client: HTTPClient) {}
}

// Unit-test
func test_load() {
  let spy = HTTPClientSpy()
  let loader = StationLoader(client: spy)
  ...
}

private class HTTPClientSpy: HTTPClient {
  func load(url: URL) {}
}

Nhiều lợi ích hơn từ giao thức, tôi có thể dễ dàng thay đổi lớp triển khai từ giao thức và không thay đổi hành vi của mã sản xuất

public protocol HTTPClient {
  func load(url: URL)
}

public class URLSessionHTTPClient: HTTPClient {
  func load(url: URL) {}
}

public class AlamofireHTTPClient: HTTPClient {
  func load(url: URL) {}  
}

// Main Module
class AnyClass {
  private let client: HTTPClient

  init(client: HTTPClient) {}

  func request(url: URL) {
    client.load(url: url)
  }
}

let aClass = AnyClass(client: URLSessionHTTPClient())
// or
let aClass = AnyClass(client: AlamofireHTTPClient())

aClass.request(url: URL(string: "https://any-url.com")!)

Cảm ơn bạn đã đọc. Hẹn gặp lại sau!!