В коде, который вы хотите протестировать, нужно указать несколько вещей:
- Используется неявная зависимость от третьей стороны , службы aws sdk.Поведение компонента с неявными зависимостями сложнее проверить, потому что вы не можете управлять им напрямую
- . Это делает асинхронный вызов .То есть часть поведения происходит в обратном вызове закрытия метода
initialize
.Это означает, что тесты должны будут учитывать тот факт, что они должны ждать вызова закрытия.
Как отмечает Энрике Бермудес , вы всегда должны избегать создания сетизвонки в юнит или интеграционные тесты.При попадании на сервер тесты запускаются медленнее, и тесты должны быть быстрыми , чтобы вы могли быстро получить обратную связь.Кроме того, вы хотите, чтобы ваши тесты были предсказуемыми, но выполнение сетевого вызова может привести к сбою по ряду причин, например, из-за тайм-аута, сбоя сервера или сбоя подключения.
Лучший способЯ знаю, как изолировать себя от зависимости от сервера и стороннего кода для AWS SDK - это поставить перед ним протокол и заменить его на двойной тест.
protocol AWSSDKWrapper {
static func initialize() // this should actually match the signature of the
// initialize method on AWSSDKService, but I don't
// know how it looks like
}
extension AWSSDKService: AWSSDKWrapper { }
Вы можетезатем вставьте зависимость от AWS в init
вашего типа.
class HomeInteractor {
private let awsSDKWrapper: AWSSDKWrapper
init(awsSDKWrapper: AWSSDKWrapper = AWSSDKService.self) {
self.awsSDKWrapper = awsSDKWrapper
}
func initSDK () {
let launchParams: [String:String] = // ...
awsSDKWrapper.initialize(withLaunchParams: launchParams) {[weak self] (success, error) in
if success {
self?.myPresenter?.onSDKInitlized()
self?.didSdkInitilized = true
} else {
self?.myPresenter?.onError(errorText: error?.localizedDescription ?? "")
}
}
}
}
Теперь у вас есть уровень абстракции между третьей стороной и сетевым запросом, который он делает.Мы можем построить наш собственный двойной тест для контроля поведения в тестах.
struct AWSSDKStub: AWSSDKWrapper {
let result: Result<Bool, NSError> // I'm assuming the type of error the AWSSDK
// callback returns is NSError.
// I'm also assuming you have Swift 5 Result
// available, if you don't check this Gist for
// a lightweight drop-in replacement
init(succeeding: Bool) {
self.result = .success(succeeding)
}
init(error: NSError) {
self.result = .failure(error)
}
func initialize(/* again not sure how the arguments look like */) {
switch result {
case .success(let succeeded): callback(succeeded)
case .failure(let error): error
}
}
}
В зависимости от того, какое поведение вы хотите проверить, вы можете или не хотите добавлять значение пробника кзахватите launchParams, переданные для инициализации.
Теперь давайте используем этот объект для управления поведением в тестах.
func testAWSSDKInitializeSuccess() {
let homeInteractor = HomeInteractor(awsSDWWrapper: AWSSDKStub(succeeding: true))
// Because the test is asynchronous we need to setup the expectation _before_
// calling its method.
//
// More here: https://www.mokacoding.com/blog/xctest-closure-based-expectation/
let predicate = NSPredicate(block: { any, _ in
return (any as? HomeIterator)?.didSdkInitilized == true
})
_ = self.expectation(for: predicate, evaluatedWith: homeIterator, handler: .none)
homeIntera.initSDK()
waitForExpectations(timeout: 1, handler: .none)
}
Прелесть этого подхода в том, что мы также можем писать тесты для случаяв котором инициализация AWS SDK возвращает false или ошибку.Мы не смогли бы сделать это, если бы продолжали использовать конечную точку AWS, поскольку у нас нет контроля над тем, как она реагирует.
Дополнительные примечания
Включение сторонних зависимостей в протоколы, которыеПредоставлять только тот API, который интересует наши приложения, ценно не только потому, что оно позволяет нам предоставлять тестовые дубликаты и лучше тестировать взаимодействие кода с зависимостями, но также и потому, что оно позволяет нам не менять код при изменении зависимости,только обертка.См. Также принцип инверсии зависимости .
Вызов протокола оболочки Wrapper
- это запах.В идеале вы должны использовать имя, которое фиксирует подмножество используемых вами функций сторонних производителей.
Написанный нами тест проверяет, установлено ли для didSdkInitilized
значение true
.Вопрос, который я хотел бы задать вам: «Это фактическое поведение HomeInteractor
или это просто деталь реализации?»Трудно ответить, не зная больше о том, что должен делать HomeIterator, но я предполагаю, что didSdkInitilized
- это только деталь реализации, и реальное поведение метода заключается в вызове onSDKInitlized()
или onError(errorText:)
для его презентатора.
Всегда лучше написать тест, ориентированный на поведение, а не на детали реализации.Тесты, сфокусированные на деталях реализации, мешают вам, когда вы хотите провести рефакторинг кода, то есть изменить его реализацию, не меняя его поведения.Тест, сфокусированный на поведении, поможет вам в долгосрочной перспективе.
В вашем случае возможный способ проверить, вызывает ли HomeInteractor
презентатор при успешной инициализации стороннего SDK, - это использовать двойной тест длядокладчик, а затем проверить, были ли вызваны его методы.Подробнее об этом подходе здесь .
Надеюсь, это поможет.Напишите мне в Твиттере на @ mokagio , если вы хотите больше поговорить о тестировании в Swift.