Irbisa · Шпаргалка
Тестирование в iOS
XCTest, Swift Testing, XCUITest, тестовые дублёры, async-тесты, снапшоты, покрытие, флаки
Чем отличаются unit-, интеграционные и UI-тесты, и зачем вообще писать тесты для iOS-приложения?
Как выглядит хороший unit-тест, и что такое Arrange-Act-Assert?
@Test("a coupon below its minimum spend is ignored")func couponBelowMinimum() { // Arrange var cart = Cart(items: [Item(price: 20)]) let coupon = Coupon(code: "SPRING", discount: 10, minimumSpend: 50)…Что делает
@testable import, и что он требует от сборки?Что делают
setUpиtearDownв XCTest, и сколько экземпляров класса тест-кейса создаёт XCTest?final class CartTests: XCTestCase { private var defaults: UserDefaults! private var sut: Cart! override func setUpWithError() throws { // A private suite, not .standard: nothing bleeds into the next test…Этот тест зелёный, хотя
FailingAPIвсегда возвращает ошибку. Почему ассерты никогда не падают?final class ProfileViewModelTests: XCTestCase { func testLoadReportsOffline() { let sut = ProfileViewModel(api: FailingAPI(error: .offline)) sut.load { result in XCTAssertEqual(result, .failure(.offline)) XCTAssertEqual(sut.state, .error) } }} // ProfileViewModel: the completion fires after an awaitfunc load(completion: @escaping (Result<Profile, APIError>) -> Void) { Task { completion(await api.fetch()) }}Чем
#expectотличается от#requireв Swift Testing, и что они показывают такого, чего не можетXCTAssertTrue?import Testing @Suite("Cart")struct CartTests { @Test func total() throws { let cart = Cart(items: [Item(price: 10), Item(price: 15)])…Как UI-тест на XCUITest взаимодействует с приложением, и чем он отличается от unit-теста?
final class CheckoutUITests: XCTestCase { func testPayShowsConfirmation() { let app = XCUIApplication() app.launchArguments = ["-uiTesting"] app.launch()…Чем Swift Testing отличается от XCTest, и что при миграции должно остаться в XCTest?
// Before: XCTestfinal class CartTests: XCTestCase { private var sut: Cart! override func setUp() { sut = Cart(pricing: StubPricing()) } func testDiscount() {…Сколько кейсов запускает каждый из этих параметризованных тестов, и что пойдёт не так, если списки в
zipразной длины?enum Plan: CaseIterable { case free, pro, team } @Test(arguments: Plan.allCases, [1, 5, 50])func priceIsNeverNegative(plan: Plan, seats: Int) { #expect(Pricing.price(plan, seats: seats) >= 0)} @Test(arguments: zip(Plan.allCases, [0, 9, 29]))func basePrice(plan: Plan, expected: Int) { #expect(Pricing.price(plan, seats: 1) == expected)}Что такое stub, spy, mock и fake, и как сделать их в Swift без библиотеки для моков?
protocol ProfileLoading { func profile(id: String) async throws -> Profile} // Stub + spy: returns what the test needs and records the callsfinal class ProfileLoaderSpy: ProfileLoading {…Найдите баг: этот тест падает, хотя
CheckoutViewModel.confirm()действительно вызываетtrack("purchase").protocol Analytics { mutating func track(_ event: String) }struct AnalyticsSpy: Analytics { private(set) var events: [String] = [] mutating func track(_ event: String) { events.append(event) }}final class CheckoutViewModel { private var analytics: any Analytics init(analytics: any Analytics) { self.analytics = analytics } func confirm() { analytics.track("purchase") }}@Test func confirmTracksPurchase() { let spy = AnalyticsSpy() let sut = CheckoutViewModel(analytics: spy) sut.confirm() #expect(spy.events == ["purchase"])}Почему
wait(for:timeout:)зависает вasync-тесте XCTest, и когда при async/await всё ещё нужен expectation?// Wrong: blocks the thread the callback needsfunc testRefreshes_hangs() async { let exp = expectation(description: "refreshed") sut.refresh { _ in exp.fulfill() } wait(for: [exp], timeout: 1)}…Тест вызывает
load()у@MainActorview model и сразу проверяет состояние, а оно всё ещё.idle. Почему, и как исправить тест?@MainActor @Observablefinal class ProfileViewModel { private(set) var state: State = .idle private(set) var loadTask: Task<Void, Never>? private let loader: ProfileLoading…Как доказать, что событие аналитики так и не отправилось, не платя в каждом прогоне пять секунд ожидания на inverted expectation?
final class AnalyticsSpy: AnalyticsTracking { private(set) var events: [Event] = [] var onTrack: ((Event) -> Void)? func track(_ event: Event) { events.append(event)…Что проверяет teardown-блок в этом тесте, и почему он падает для этой view model?
final class FeedViewModel { private let service: FeedService private(set) var items: [Post] = [] init(service: FeedService) { self.service = service service.onUpdate = { posts in self.items = posts } }} func testFeedViewModelIsReleased() { let sut = FeedViewModel(service: FeedService()) addTeardownBlock { [weak sut] in XCTAssertNil(sut, "FeedViewModel leaked") }}Как тестировать SwiftUI-view, если в unit-тесте нельзя посмотреть, что отрисовал
body?@Observablefinal class CartModel { private(set) var items: [Item] = [] var isSubmitting = false var canBuy: Bool { !items.isEmpty && !isSubmitting }…Что такое snapshot-тесты, и почему они так часто падают на CI с едва заметным диффом?
import SnapshotTestingimport XCTest final class ProfileHeaderSnapshotTests: XCTestCase { // Same destination locally and on CI, e.g. // -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4'…Сьют XCUITest зелёный на английском и красный в день, когда пришла немецкая сборка. По чему матчились запросы, и по чему должны?
struct CheckoutView: View { let total: Decimal var body: some View { VStack { Text(total, format: .currency(code: "EUR"))…UI-тесту нужен залогиненный пользователь с тремя товарами в корзине. Как привести приложение в это состояние, не проходя вход нажатиями?
// Test targetfunc testCheckoutFromSeededCart() { let app = XCUIApplication() app.launchArguments = ["-uiTesting"] app.launchEnvironment = ["SCENARIO": "signed_in_cart_3"] app.launch()…Что на самом деле измеряет покрытие кода в Xcode, и почему view model с покрытием 87% всё равно уезжает с багом?
Что такое TDD, и где в iOS-разработке он окупается, а где нет?
Как настроить тестовые джобы iOS-пайплайна: что запускается на каждом pull request, а что на мерж или по ночам?
Включение параллельного тестирования сократило прогон с 20 минут до 6, и четыре теста начали падать случайно. Что изменила параллельность?
// Wrong: a global the tests mutate; in parallel they raceenum Session { static var current: User? } @Test func signInStoresUser() { Session.current = User(id: "1") #expect(Session.current?.id == "1") // fails whenever another test wins…Один XCUITest падает примерно в одном из восьми прогонов на CI и ни разу на вашем ноутбуке. Как отличить настоящую гонку от недостающего ожидания?
final class CheckoutUITests: XCTestCase { private var app: XCUIApplication! override func setUp() { continueAfterFailure = false app = XCUIApplication()…View controller на 900 строк вызывает
AnalyticsManager.shared,URLSession.shared,UserDefaults.standardиDate(). Как покрыть его тестами уже на этой неделе?// After: seams only, the singletons stay as they areprotocol Analytics { func track(_ event: Event) }extension AnalyticsManager: Analytics {} final class ReceiptPresenter { private let session: URLSession…