В коде я делаю это так:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateBadgeValuesForTabBarItems()
}
private func updateBadgeValuesForTabBarItems() {
DispatchQueue.main.async {
self.setBadge(value: self.viewModel.numberOfUnreadMessages, for: .threads)
self.setBadge(value: self.viewModel.numberOfActiveTasks, for: .tasks)
self.setBadge(value: self.viewModel.numberOfUnreadNotifications, for: .notifications)
}
}
и в тестах:
func testViewDidAppear() {
let view = TabBarView()
let model = MockTabBarViewModel()
let center = NotificationCenter()
let controller = TabBarController(view: view, viewModel: model, notificationCenter: center)
controller.viewDidLoad()
XCTAssertFalse(model.numberOfActiveTasksWasCalled)
XCTAssertFalse(model.numberOfUnreadMessagesWasCalled)
XCTAssertFalse(model.numberOfUnreadNotificationsWasCalled)
XCTAssertFalse(model.indexForTypeWasCalled)
controller.viewDidAppear(false)
XCTAssertTrue(model.numberOfActiveTasksWasCalled) //failed
XCTAssertTrue(model.numberOfUnreadMessagesWasCalled) //failed
XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled) //failed
XCTAssertTrue(model.indexForTypeWasCalled) //failed
}
Но все четыре моих последних утверждения не оправдались. Почему? Как я могу его успешно протестировать?
Дело не в этом. Любой пример с МОИМ кодом ?;)
Избавьтесь от DispatchQueue.main.async
в вашем методе.
Вам не нужно вызывать код метода updateBadgeValuesForTabBarItems
в основной очереди.
Но если вам это действительно нужно, вы можете сделать что-то вроде этого:
func testViewDidAppear() {
let view = TabBarView()
let model = MockTabBarViewModel()
let center = NotificationCenter()
let controller = TabBarController(view: view, viewModel: model, notificationCenter: center)
controller.viewDidLoad()
XCTAssertFalse(model.numberOfActiveTasksWasCalled)
XCTAssertFalse(model.numberOfUnreadMessagesWasCalled)
XCTAssertFalse(model.numberOfUnreadNotificationsWasCalled)
XCTAssertFalse(model.indexForTypeWasCalled)
controller.viewDidAppear(false)
let expectation = self.expectation(description: "Test")
DispatchQueue.main.async {
expectation.fullfill()
}
self.waitForExpectations(timeout: 1, handler: nil)
XCTAssertTrue(model.numberOfActiveTasksWasCalled)
XCTAssertTrue(model.numberOfUnreadMessagesWasCalled)
XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled)
XCTAssertTrue(model.indexForTypeWasCalled)
}
Но это не лучшая практика.
Умный подход, поскольку мы знаем, что асинхронная отправка из тестов обязательно будет запускаться после отправки из контроллера. Как вы сказали, это не то, что нужно использовать в производственном коде, но для тестов это должно быть нормально.
Вот небольшое доказательство того, как вы могли этого добиться:
func testExample() {
let expectation = self.expectation(description: "numberOfActiveTasks")
var mockModel = MockModel()
mockModel.numberOfActiveTasksClosure = {() in
expectation.fulfill()
}
DispatchQueue.main.async {
_ = mockModel.numberOfActiveTasks
}
self.waitForExpectations(timeout: 2, handler: nil)
}
а вот и MockModel
:
struct MockModel : Model {
var numberOfActiveTasks: Int {
get {
if let cl = numberOfActiveTasksClosure {
cl()
}
//we dont care about the actual value for this test
return 0
}
}
var numberOfActiveTasksClosure: (() -> ())?
}
Я думаю, что лучший способ проверить это - издеваться над DispatchQueue
. Вы можете создать протокол, определяющий функции, которые вы хотите использовать:
protocol DispatchQueueType {
func async(execute work: @escaping @convention(block) () -> Void)
}
Теперь расширите DispatchQueue
, чтобы он соответствовал вашему протоколу, например:
extension DispatchQueue: DispatchQueueType {
func async(execute work: @escaping @convention(block) () -> Void) {
async(group: nil, qos: .unspecified, flags: [], execute: work)
}
}
Обратите внимание, что мне пришлось исключить из протокола параметры, которые вы не использовали в своем коде, такие как group
, qos
и flags
, поскольку протокол не допускает значений по умолчанию. Вот почему расширение должно было явно реализовывать функцию протокола.
Теперь в ваших тестах создайте имитацию DispatchQueue
, которая соответствует этому протоколу и синхронно вызывает закрытие, например:
final class DispatchQueueMock: DispatchQueueType {
func async(execute work: @escaping @convention(block) () -> Void) {
work()
}
}
Теперь все, что вам нужно сделать, это соответствующим образом ввести очередь, возможно, в контроллере представления init
, например:
final class ViewController: UIViewController {
let mainDispatchQueue: DispatchQueueType
init(mainDispatchQueue: DispatchQueueType = DispatchQueue.main) {
self.mainDispatchQueue = mainDispatchQueue
super.init(nibName: nil, bundle: nil)
}
func foo() {
mainDispatchQueue.async {
*perform asynchronous work*
}
}
}
Наконец, в ваших тестах вам нужно создать контроллер представления, используя имитацию очереди отправки, например:
func testFooSucceeds() {
let controller = ViewController(mainDispatchQueue: DispatchQueueMock())
controller.foo()
*assert work was performed successfully*
}
Поскольку вы использовали имитацию очереди в своем тесте, код будет выполняться синхронно, и вам не нужно мучительно ждать ожиданий.
Вы можете легко добиться этого, проверив, является ли текущий поток основным, и в этом случае выполнить код синхронно.
Например, в презентаторе я обновляю представление следующим образом:
private func updateView(with viewModel: MyViewModel) {
if Thread.isMainThread {
view?.update(with: viewModel)
} else {
DispatchQueue.main.async {
self.view?.update(with: viewModel)
}
}
}
И тогда я могу написать синхронные модульные тесты для моего докладчика:
func testOnViewDidLoadFetchFailed() throws {
presenter.onViewDidLoad()
// presenter is calling interactor.fetchData when onViewDidLoad is called
XCTAssertEqual(interactor.fetchDataCallsCount, 1)
// test execute fetchData completion closure manually in the main thread
interactor.fetchDataCalls[0].completion(.failure(TestError()))
// presenter will call updateView(viewModel:) internally in synchronous way
// because we have check if Thread.isMainThread in updateView(viewModel:)
XCTAssertEqual(view.updateCallsCount, 1)
guard case .error = view.updateCalls[0] else {
XCTFail("error expected, got \(view.updateCalls[0])")
return
}
}
Но код из вопроса уже находится в основном потоке, так как он вызывается из viewDidAppear
...
не имеет значения, выполняется ли он в основном потоке. Потому что в случае DispatchQueue.main.async
он будет выполняться не синхронно и с некоторой задержкой. Вот почему вы не можете писать синхронные тесты
Верно, но предлагаемое здесь изменение меняет поведение производственного кода, и это может быть нежелательно.
Чтобы протестировать асинхронный код, вы должны изменить свою функцию updateBadgeValuesForTabBarItems
и вызвать ее прямо из ваших тестов с закрытием завершения:
func updateBadgeValuesForTabBarItems(completion: (() -> Void)? = nil) {
DispatchQueue.main.async {
self.setBadge(value: self.viewModel.numberOfUnreadMessages, for: .threads)
self.setBadge(value: self.viewModel.numberOfActiveTasks, for: .tasks)
self.setBadge(value: self.viewModel.numberOfUnreadNotifications, for: .notifications)
completion?()
}
}
Теперь вы можете вызывать эту функцию, как раньше, в обычном коде, например: updateBadgeValuesForTabBarItems()
. Но для тестов вы можете добавить завершение закрытия и использовать XCTestExpectation
для ожидания:
func testBadge() {
...
let expectation = expectation(description: "Badge")
updateBadgeValuesForTabBarItems {
XCTAssertTrue(model.numberOfActiveTasksWasCalled)
XCTAssertTrue(model.numberOfUnreadMessagesWasCalled)
XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled)
XCTAssertTrue(model.indexForTypeWasCalled)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
Так что, по сути, отправить какой-нибудь код в AppStore, который будет использоваться только тестами? Не похоже на хорошую идею ...
Кроме того, для этого требуется раскрыть текущий частный метод updateBadgeValuesForTabBarItems
, что может не иметь смысла с архитектурной точки зрения.
Вам следует
Применим эти три пункта:
Чтобы инвертировать зависимость, нам понадобится тип Аннотация, то есть в Swift протокол. Затем мы расширяем DispatchQueue, чтобы соответствовать этому протоколу.
protocol Dispatching {
func async(execute workItem: DispatchWorkItem)
}
extension DispatchQueue: Dispatching {}
Затем нам нужно внедрить зависимость в наш контроллер представления. Это означает, что все, что отправляется, передается нашему контроллеру представления.
final class MyViewController {
// MARK: - Dependencies
private let dispatchQueue: Dispatching // Declading that our class needs a dispatch queue
// MARK: - Initialization
init(dispatchQueue: Dispatching = DispatchQueue.main) { // Injecting the dependencies via constructor
self.dispatchQueue = dispatchQueue
super.init(nibName: nil, bundle: nil) // We must call super
}
@available(*, unavailable)
init(coder aCoder: NSCoder?) {
fatalError("We should only use our other init!")
}
// MARK: - View lifecycle
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateBadgeValuesForTabBarItems()
}
// MARK: - Private methods
private func updateBadgeValuesForTabBarItems() {
dispatchQueue.async { // Using our dependency instead of DispatchQueue directly
self.setBadge(value: self.viewModel.numberOfUnreadMessages, for: .threads)
self.setBadge(value: self.viewModel.numberOfActiveTasks, for: .tasks)
self.setBadge(value: self.viewModel.numberOfUnreadNotifications, for: .notifications)
}
}
}
Наконец, нам нужно создать макет для наших тестов. В этом случае, следуя двойные испытания, мы должны создать Fake, то есть макет DispatchQueue, который на самом деле не работает в продакшене, но работает в наших тестах.
final class DispatchFake: Dispatching {
func async(execute workItem: DispatchWorkItem) {
workItem.perform()
}
}
Когда мы тестируем, все, что нам нужно сделать, это создать нашу тестируемую систему (в данном случае контроллер), передав поддельный экземпляр диспетчеризации.
Похоже, это то же самое решение, что и самый популярный ответ.
Я считаю, что это лучшее объяснение, чем «создать протокол, расширить очередь отправки и смоделировать его в ваших тестах». Это дает намного больше информации о том, почему мы делаем эти вещи, и на основе лучших принципов программирования дает понять, почему это отличное решение.
проверьте medium.com/@johnsundell/…