Как я могу издеваться над URLSession? - PullRequest
0 голосов
/ 02 октября 2018

Я смотрю на приложение для iOS, написанное на Swift 4. У него довольно простой сетевой уровень, использующий URLSession, однако приложение не имеет модульных тестов, и, прежде чем начать рефакторинг, я хочу решить эту проблему, представив рядtests.

Прежде чем я смогу это сделать, я должен иметь возможность макетировать URLSession, чтобы не создавать реальных сетевых запросов во время тестирования.Я не вижу в текущей реализации, как я могу добиться этого?Где находится точка входа для моего ввода URLSession в моих тестах.

Я извлек сетевой код и создал простое приложение, используя ту же логику, которая выглядит следующим образом:

Конечная точка.swift

import Foundation

protocol Endpoint {
    var baseURL: String { get }
}

extension Endpoint {
    var urlComponent: URLComponents {
        let component = URLComponents(string: baseURL)
        return component!
    }

    var request: URLRequest {
        return URLRequest(url: urlComponent.url!)
    }
}

struct RandomUserEndpoint: Endpoint {
    var baseURL: String {
        return RandomUserClient.baseURL
    }
}

APIClient.swift

import Foundation

enum Either<T> {
    case success(T), error(Error)
}

enum APIError: Error {
    case unknown, badResponse, jsonDecoder
}

enum HTTPMethod: String {
    case get = "GET"
    case put = "PUT"
    case post = "POST"
    case patch = "PATCH"
    case delete = "DELETE"
    case head = "HEAD"
    case options = "OPTIONS"
}

protocol APIClient {
    var session: URLSession { get }
    func get<T: Codable>(with request: URLRequest, completion: @escaping (Either<T>) -> Void)
}

extension APIClient {
    var session: URLSession {
        return URLSession.shared
    }

   func get<T: Codable>(with request: URLRequest, completion: @escaping (Either<T>) -> Void) {
        let task = session.dataTask(with: request) { (data, response, error) in
            guard error == nil else { return completion(.error(error!)) }
            guard let response = response as? HTTPURLResponse, 200..<300 ~= response.statusCode else { completion(.error(APIError.badResponse)); return }

            guard let data = data else { return }

            guard let value = try? JSONDecoder().decode(T.self, from: data) else { completion(.error(APIError.jsonDecoder)); return }

            DispatchQueue.main.async {
                completion(.success(value))
            }
        }
        task.resume()
    }

}

RandomUserClient.swift

import Foundation

class RandomUserClient: APIClient {
    static let baseURL = "https://randomuser.me/api/"

    func fetchRandomUser(with endpoint: RandomUserEndpoint, method: HTTPMethod, completion: @escaping (Either<RandomUserResponse>)-> Void) {
        var request = endpoint.request
        request.httpMethod = method.rawValue
        get(with: request, completion: completion)
    }

}

RandomUserModel.swift

import Foundation

typealias RandomUser = Result

struct RandomUserResponse: Codable {
    let results: [Result]?
}

struct Result: Codable {
    let name: Name
}

struct Name: Codable {
    let title: String
    let first: String
    let last: String
}

Очень простое приложение для использования этого кода может быть что-то вроде

class ViewController: UIViewController {

    let fetchUserButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("FETCH", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 36)
        button.addTarget(self, action: #selector(fetchRandomUser), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.isEnabled = true
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        view.addSubview(fetchUserButton)
        NSLayoutConstraint.activate([
            fetchUserButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            fetchUserButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    @objc func fetchRandomUser() {
        let client = RandomUserClient()
        fetchUserButton.isEnabled = false
        client.fetchRandomUser(with: RandomUserEndpoint(), method: .get) { [unowned self] (either) in
            switch either {
            case .success(let user):
                guard let name = user.results?.first?.name else { return }
                let message = "Your new name is... \n\(name.first.uppercased()) \(name.last.uppercased())"
                self.showAlertView(title: "", message: message)
                self.fetchUserButton.isEnabled = true
            case .error(let error):
                print(error.localizedDescription)
            }
        }
    }

    func showAlertView(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
        alert.addAction(UIAlertAction(title: "Close", style: UIAlertAction.Style.default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}

В идеале я хотел бы, чтобы способ макетировать URLSession так что я могу проверить это правильно, однако я не уверен, как я могу добиться этого с текущим кодом.

1 Ответ

0 голосов
/ 02 октября 2018

В этом случае на самом деле это может иметь больше смысла, если вы утверждаете около RandomUserClient.

Вы расширяете RandomUserClient, и он принимает экземпляр URLSession, который сам вводится в ваш APIClient.

class RandomUserClient: APIClient {
    var session: URLSession
    static let baseURL = "https://randomuser.me/api/"

    init(session: URLSession) {
        self.session = session
    }

    func fetchRandomUser(with endpoint: RandomUserEndpoint, method: HTTPMethod, completion: @escaping (Either<RandomUserResponse>)-> Void) {
        var request = endpoint.request
        request.httpMethod = method.rawValue

        get(with: request, session: session, completion: completion)
    }

}

Ваш контроллер представления должен быть обновлен так,RandomUserClient инициализируется примерно так: lazy var client = RandomUserClient(session: URLSession.shared)

Ваш протокол и расширение APIClient также необходимо реорганизовать для принятия новой введенной зависимости URLSession

protocol APIClient {
    func get<T: Codable>(with request: URLRequest, session: URLSession, completion: @escaping (Either<T>) -> Void)
}

extension APIClient {
    func get<T: Codable>(with request: URLRequest, session: URLSession, completion: @escaping (Either<T>) -> Void) {
        let task = session.dataTask(with: request) { (data, response, error) in
            guard error == nil else { return completion(.error(error!)) }
            guard let response = response as? HTTPURLResponse, 200..<300 ~= response.statusCode else { completion(.error(APIError.badResponse)); return }

            guard let data = data else { return }

            guard let value = try? JSONDecoder().decode(T.self, from: data) else { completion(.error(APIError.jsonDecoder)); return }

            DispatchQueue.main.async {
                completion(.success(value))
            }
        }
        task.resume()
    }

}

Обратите внимание на добавление session: URLSession.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...