From a7da60e621a6b13acd8e9119b62ebee813b960bf Mon Sep 17 00:00:00 2001 From: Pyry Jahkola Date: Tue, 7 Jan 2025 06:57:40 +0200 Subject: [PATCH] Fix unit tests for Xcode 16 and older iOS destinations (#3537) * Limit test case availability when using TestClock * Use withLock with shared state * Fix unit tests on iOS 16 and earlier * Fix DEBUG-mode perception check test to cover iOS 17+ * Run iOS & macOS unit tests on Xcode 16.0 --- .github/workflows/ci.yml | 2 +- .../Reducers/OnChangeReducerTests.swift | 6 +-- .../StorePerceptionTests.swift | 48 ++++++++++++------- .../StoreTests.swift | 2 + 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c33f9dc56fa..de7dac9b379d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: macos-15 strategy: matrix: - command: [''] + command: [test, ''] platform: [IOS, MACOS] xcode: ['16.0'] steps: diff --git a/Tests/ComposableArchitectureTests/Reducers/OnChangeReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/OnChangeReducerTests.swift index e0037d0dae69..03ac25947f32 100644 --- a/Tests/ComposableArchitectureTests/Reducers/OnChangeReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/OnChangeReducerTests.swift @@ -208,7 +208,7 @@ final class OnChangeReducerTests: BaseTCATestCase { Reduce { state, action in switch action { case .incrementButtonTapped: - state.count.value += 1 + state.$count.withLock { $0.value += 1 } return .none } } @@ -222,11 +222,11 @@ final class OnChangeReducerTests: BaseTCATestCase { } let store = await TestStore(initialState: Feature.State()) { Feature() } await store.send(.incrementButtonTapped) { - $0.count.value = 1 + $0.$count.withLock { $0.value = 1 } $0.description = "old: 0, new: 1" } await store.send(.incrementButtonTapped) { - $0.count.value = 2 + $0.$count.withLock { $0.value = 2 } $0.description = "old: 1, new: 2" } } diff --git a/Tests/ComposableArchitectureTests/StorePerceptionTests.swift b/Tests/ComposableArchitectureTests/StorePerceptionTests.swift index d43e8d4b00d4..fceb4fe936d8 100644 --- a/Tests/ComposableArchitectureTests/StorePerceptionTests.swift +++ b/Tests/ComposableArchitectureTests/StorePerceptionTests.swift @@ -4,6 +4,10 @@ import XCTest @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) final class StorePerceptionTests: BaseTCATestCase { + override func setUpWithError() throws { + try checkAvailability() + } + @MainActor func testPerceptionCheck_SkipWhenOutsideView() { let store = Store(initialState: Feature.State()) { @@ -29,25 +33,30 @@ final class StorePerceptionTests: BaseTCATestCase { @MainActor func testPerceptionCheck_AccessStateWithoutTracking() { - if #unavailable(iOS 17, macOS 14, tvOS 17, watchOS 10) { - @MainActor - struct FeatureView: View { - let store = Store(initialState: Feature.State()) { - Feature() - } - var body: some View { - Text(store.count.description) - } + @MainActor + struct FeatureView: View { + let store = Store(initialState: Feature.State()) { + Feature() } - XCTExpectFailure { - render(FeatureView()) - } issueMatcher: { - $0.compactDescription == """ - Perceptible state was accessed but is not being tracked. Track changes to state by \ - wrapping your view in a 'WithPerceptionTracking' view. - """ + var body: some View { + Text(store.count.description) } } +#if DEBUG && !os(visionOS) + let previous = Perception.isPerceptionCheckingEnabled + Perception.isPerceptionCheckingEnabled = true + defer { Perception.isPerceptionCheckingEnabled = previous } + XCTExpectFailure { + render(FeatureView()) + } issueMatcher: { + $0.compactDescription == """ + failed - Perceptible state was accessed but is not being tracked. Track changes to state by \ + wrapping your view in a 'WithPerceptionTracking' view. This must also be done for any \ + escaping, trailing closures, such as 'GeometryReader', `LazyVStack` (and all lazy \ + views), navigation APIs ('sheet', 'popover', 'fullScreenCover', etc.), and others. + """ + } +#endif } @MainActor @@ -87,3 +96,10 @@ private struct Feature { } } } + +// NB: Workaround to XCTest ignoring `@available(...)` attributes. +private func checkAvailability() throws { + guard #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) else { + throw XCTSkip("Requires iOS 16, macOS 13, tvOS 16, or watchOS 9") + } +} diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index 7ef9bccebecd..76e51811af07 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -1179,6 +1179,7 @@ final class StoreTests: BaseTCATestCase { #if canImport(Testing) @Suite struct ModernStoreTests { + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) @Reducer fileprivate struct TaskTreeFeature { let clock: TestClock @@ -1206,6 +1207,7 @@ final class StoreTests: BaseTCATestCase { } } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) @MainActor @Test func cancellation() async throws {