Выделите протокол из URLSession.shared, чтобы view controller стал тестируемым
ProfileViewController.loadProfile(from:) напрямую зовёт URLSession.shared.data(from:), поэтому модульный тест ходил бы в реальную сеть. Отрефакторьте под тестируемость: определите протокол, сделайте контроллер зависящим от него и напишите fake для тестов.
Требования:
- Определите протокол
DataFetchingс тем одним методом, что использует контроллер. - Внедрите
DataFetchingв контроллер со значением по умолчаниюURLSession.sharedв проде. - Напишите
FakeFetcher, соответствующийDataFetching, который отдаёт заготовленные данные.
final class ProfileViewController {
func loadProfile(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
Допишите реализацию.
Определите протокол DataFetching с нужным методом, сделайте URLSession соответствующим ему и внедрите DataFetching со значением по умолчанию .shared. Тесты подставляют FakeFetcher с заготовленными данными или ошибкой; сеть не трогается.
- ✗Пытаются наследовать или переопределить
URLSessionвместо протокола-обёртки - ✗Переприсваивают глобальный синглтон
sharedвместо внедрения зависимости - ✗Оставляют контроллер зовущим
URLSession.sharedвнутри после рефакторинга
- →Почему инъекция протокола чище, чем наследование от
URLSession? - →Как значение по умолчанию у параметра
initсохраняет прод-код без правок?
The protocol hides the concrete session; production defaults to the real one, tests pass a fake.
protocol DataFetching {
func data(from url: URL) async throws -> (Data, URLResponse)
}
extension URLSession: DataFetching {} // already has this exact method
final class ProfileViewController {
private let fetcher: DataFetching
init(fetcher: DataFetching = URLSession.shared) { self.fetcher = fetcher }
func loadProfile(from url: URL) async throws -> Data {
let (data, _) = try await fetcher.data(from: url)
return data
}
}
// In tests:
struct FakeFetcher: DataFetching {
var result: Result<Data, Error>
func data(from url: URL) async throws -> (Data, URLResponse) {
(try result.get(), URLResponse())
}
}
Production call sites need no change — the default parameter keeps URLSession.shared. The test constructs ProfileViewController(fetcher: FakeFetcher(result: .success(json))), so loadProfile runs against canned bytes with no network, and an error case is just .failure(...).