diff --git a/Tests/OneWayTestingTests/TestingTests.swift b/Tests/OneWayTestingTests/TestingTests.swift index 9425ea5..6b0b389 100644 --- a/Tests/OneWayTestingTests/TestingTests.swift +++ b/Tests/OneWayTestingTests/TestingTests.swift @@ -84,7 +84,7 @@ private struct TestReducer: Reducer { return .none case .delayedIncrement: return .single { - try! await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + try! await Task.sleep(for: .milliseconds(100)) return .increment } case let .setName(name): diff --git a/Tests/OneWayTestingTests/XCTestTests.swift b/Tests/OneWayTestingTests/XCTestTests.swift index 7fc105b..c844a32 100644 --- a/Tests/OneWayTestingTests/XCTestTests.swift +++ b/Tests/OneWayTestingTests/XCTestTests.swift @@ -81,7 +81,7 @@ private struct TestReducer: Reducer { return .none case .delayedIncrement: return .single { - try! await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + try! await Task.sleep(for: .milliseconds(100)) return .increment } case let .setName(name): diff --git a/Tests/OneWayTests/EffectTests.swift b/Tests/OneWayTests/EffectTests.swift index 06f2fe8..aa25354 100644 --- a/Tests/OneWayTests/EffectTests.swift +++ b/Tests/OneWayTests/EffectTests.swift @@ -5,12 +5,12 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // +import Testing import Clocks import OneWay -import XCTest -final class EffectTests: XCTestCase { - enum Action: Sendable { +struct EffectTests { + enum Action: Sendable, Equatable { case first case second case third @@ -18,7 +18,8 @@ final class EffectTests: XCTestCase { case fifth } - func test_just() async { + @Test + func just() async { let values = Effects.Just(Action.first).values var result: [Action] = [] @@ -26,32 +27,35 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [.first]) + #expect(result == [.first]) } - func test_cancel() async { + @Test + func cancel() async { let cancel = AnyEffect.cancel("100") let method = cancel.method if case .cancel(let id) = method { - XCTAssertEqual(id as! String, "100") + #expect(id as? String == "100") } else { - XCTFail() + Issue.record("method should be .cancel") } } - func test_cancellable() async { + @Test + func cancellable() async { let effect = Effects.Just("").eraseToAnyEffect().cancellable("100") let method = effect.method if case let .register(id, _) = method { - XCTAssertEqual(id as! String, "100") + #expect(id as? String == "100") } else { - XCTFail() + Issue.record("method should be .register") } } - func test_single() async { + @Test + func single() async { let clock = TestClock() let values = Effects.Single { @@ -65,10 +69,11 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [.first]) + #expect(result == [.first]) } - func test_sequence() async { + @Test + func sequence() async { let stream = AsyncStream { continuation in for number in 1 ... 5 { continuation.yield(number) @@ -87,7 +92,7 @@ final class EffectTests: XCTestCase { case 5: action = .fifth default: action = .first - XCTFail() + Issue.record("should not be reached") } send(action) } @@ -98,19 +103,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_concat() async { + @Test + func concat() async { let clock = TestClock() let first = Effects.Just(Action.first).eraseToAnyEffect() @@ -142,20 +146,18 @@ final class EffectTests: XCTestCase { result.append(value) } - - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_concatIncludingMerge() async { + @Test + func concatIncludingMerge() async { let clock = TestClock() let first = Effects.Single { @@ -191,19 +193,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_merge() async { + @Test + func merge() async { let clock = TestClock() let first = Effects.Single { @@ -241,19 +242,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_mergeIncludingConcat() async { + @Test + func mergeIncludingConcat() async { let clock = TestClock() let first = Effects.Single { @@ -289,19 +289,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_createSynchronously() async { + @Test + func createSynchronously() async { let values = Effects.Create { continuation in continuation.yield(Action.first) continuation.yield(Action.second) @@ -316,19 +315,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_createAsynchronously() async { + @Test + func createAsynchronously() async { let clock = TestClock() let values = Effects.Create { continuation in @@ -355,19 +353,18 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) } - func test_createAsynchronouslyWithCompletionHandler() async { + @Test + func createAsynchronouslyWithCompletionHandler() async { let values = Effects.Create { continuation in perform { action in continuation.yield(action) @@ -382,23 +379,22 @@ final class EffectTests: XCTestCase { result.append(value) } - XCTAssertEqual( - result, - [ - .first, - .second, - .third, - .fourth, - .fifth, - ] - ) + let expectation: [Action] = [ + .first, + .second, + .third, + .fourth, + .fifth, + ] + #expect(result == expectation) func perform(completionHandler: @Sendable @escaping (Action) -> Void) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + Task { completionHandler(.first) completionHandler(.second) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + Task { + try await Task.sleep(for: .milliseconds(100)) completionHandler(.third) completionHandler(.fourth) completionHandler(.fifth) diff --git a/Tests/OneWayTests/EffectsBuilderTests.swift b/Tests/OneWayTests/EffectsBuilderTests.swift index d307ce9..d5faf7b 100644 --- a/Tests/OneWayTests/EffectsBuilderTests.swift +++ b/Tests/OneWayTests/EffectsBuilderTests.swift @@ -5,11 +5,12 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // +import Testing import OneWay -import XCTest -final class EffectsBuilderTests: XCTestCase { - func test_array() async { +struct EffectsBuilderTests { + @Test + func array() async { do { let effect = AnyEffect.concat { let effects = [ @@ -27,7 +28,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [1, 2, 3]) + #expect(result == [1, 2, 3]) } do { @@ -47,11 +48,12 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, [1, 2, 3]) + #expect(result == [1, 2, 3]) } } - func test_emptyBlock() async { + @Test + func emptyBlock() async { do { let effect = AnyEffect.concat { } @@ -60,7 +62,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, []) + #expect(result.isEmpty) } do { @@ -71,11 +73,12 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, []) + #expect(result.isEmpty) } } - func test_block() async { + @Test + func block() async { do { let effect = AnyEffect.concat { AnyEffect.just(1) @@ -88,7 +91,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [1, 2, 3]) + #expect(result == [1, 2, 3]) } do { @@ -103,11 +106,12 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, [1, 2, 3]) + #expect(result == [1, 2, 3]) } } - func test_conditionalBlock() async { + @Test + func conditionalBlock() async { enum Order { case first case second @@ -142,7 +146,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [1, 2, 4, 6]) + #expect(result == [1, 2, 4, 6]) } do { @@ -171,11 +175,12 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, [1, 2, 4, 6]) + #expect(result == [1, 2, 4, 6]) } } - func test_optionalBlock() async { + @Test + func optionalBlock() async { let someValue: AnyEffect? = .just(1) let someValue2: AnyEffect? = .just(2) let noneValue: AnyEffect? = nil @@ -198,7 +203,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [1, 2]) + #expect(result == [1, 2]) } do { @@ -219,11 +224,12 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, [1, 2]) + #expect(result == [1, 2]) } } - func test_limitedAvailabilityBlock() async { + @Test + func limitedAvailabilityBlock() async { do { let effect = AnyEffect.concat { AnyEffect.just(1) @@ -239,7 +245,7 @@ final class EffectsBuilderTests: XCTestCase { result.append(value) } - XCTAssertEqual(result, [1, 2]) + #expect(result == [1, 2]) } do { @@ -257,7 +263,7 @@ final class EffectsBuilderTests: XCTestCase { result.insert(value) } - XCTAssertEqual(result, [1, 2]) + #expect(result == [1, 2]) } } } diff --git a/Tests/OneWayTests/PropertyWrappersTests.swift b/Tests/OneWayTests/PropertyWrappersTests.swift index 08f4f9f..1183aa6 100644 --- a/Tests/OneWayTests/PropertyWrappersTests.swift +++ b/Tests/OneWayTests/PropertyWrappersTests.swift @@ -5,15 +5,12 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // +import Testing import OneWay -import XCTest -final class PropertyWrappersTests: XCTestCase { - override func setUp() { - super.setUp() - } - - func test_copyOnWrite() { +struct PropertyWrappersTests { + @Test + func copyOnWrite() { struct Storage { @CopyOnWrite var value: Int @CopyOnWrite var optionalValue: Int? @@ -21,38 +18,39 @@ final class PropertyWrappersTests: XCTestCase { do { var storage = Storage(value: 10, optionalValue: 10) - XCTAssertEqual(storage.value, 10) - XCTAssertEqual(storage.optionalValue, 10) + #expect(storage.value == 10) + #expect(storage.optionalValue == 10) storage.value = 20 storage.optionalValue = nil - XCTAssertEqual(storage.value, 20) - XCTAssertEqual(storage.optionalValue, nil) + #expect(storage.value == 20) + #expect(storage.optionalValue == nil) storage.value = 30 storage.optionalValue = 20 - XCTAssertEqual(storage.value, 30) - XCTAssertEqual(storage.optionalValue, 20) + #expect(storage.value == 30) + #expect(storage.optionalValue == 20) } do { var storage = Storage(value: 10, optionalValue: nil) - XCTAssertEqual(storage.value, 10) - XCTAssertEqual(storage.optionalValue, nil) + #expect(storage.value == 10) + #expect(storage.optionalValue == nil) storage.value = 20 storage.optionalValue = 10 - XCTAssertEqual(storage.value, 20) - XCTAssertEqual(storage.optionalValue, 10) + #expect(storage.value == 20) + #expect(storage.optionalValue == 10) storage.value = 30 storage.optionalValue = nil - XCTAssertEqual(storage.value, 30) - XCTAssertEqual(storage.optionalValue, nil) + #expect(storage.value == 30) + #expect(storage.optionalValue == nil) } } - func test_triggered() { + @Test + func triggered() { struct Storage: Equatable { @Triggered var value: Int } @@ -62,7 +60,7 @@ final class PropertyWrappersTests: XCTestCase { var new = old new.value = 20 - XCTAssertNotEqual(old, new) + #expect(old != new) } do { @@ -70,7 +68,7 @@ final class PropertyWrappersTests: XCTestCase { var new = old new.value = 10 - XCTAssertNotEqual(old, new) + #expect(old != new) } do { @@ -79,11 +77,12 @@ final class PropertyWrappersTests: XCTestCase { old.value = 20 new.value = 20 - XCTAssertEqual(old, new) + #expect(old == new) } } - func test_ignored() { + @Test + func ignored() { struct Storage: Equatable { @Ignored var value: Int } @@ -93,7 +92,7 @@ final class PropertyWrappersTests: XCTestCase { var new = old new.value = 20 - XCTAssertEqual(old, new) + #expect(old == new) } do { @@ -101,7 +100,7 @@ final class PropertyWrappersTests: XCTestCase { var new = old new.value = 10 - XCTAssertEqual(old, new) + #expect(old == new) } do { @@ -110,7 +109,7 @@ final class PropertyWrappersTests: XCTestCase { old.value = 20 new.value = 20 - XCTAssertEqual(old, new) + #expect(old == new) } } } diff --git a/Tests/OneWayTests/StoreTests.swift b/Tests/OneWayTests/StoreTests.swift index 4853f9e..505c69c 100644 --- a/Tests/OneWayTests/StoreTests.swift +++ b/Tests/OneWayTests/StoreTests.swift @@ -5,20 +5,19 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -import Clocks +import Testing #if canImport(Combine) import Combine #endif +import Clocks import OneWay import OneWayTesting -import XCTest -final class StoreTests: XCTestCase { +struct StoreTests { private var sut: Store>! private var clock: TestClock! - override func setUp() { - super.setUp() + init() { let clock = TestClock() self.clock = clock sut = Store( @@ -28,29 +27,25 @@ final class StoreTests: XCTestCase { ) } - override func tearDown() { - super.tearDown() - sut = nil - } - - func test_initialState() async { + @Test + func initialState() async { let initialState = await sut.initialState let state = await sut.state let states = await sut.states - XCTAssertEqual(initialState, TestReducer.State(count: 0, text: "")) - XCTAssertEqual(state.count, 0) - XCTAssertEqual(state.text, "") + #expect(initialState == TestReducer.State(count: 0, text: "")) + #expect(state.count == 0) + #expect(state.text == "") for await state in states { - XCTAssertEqual(state.count, 0) - XCTAssertEqual(state.text, "") + #expect(state.count == 0) + #expect(state.text == "") break } } - func test_sendSeveralActions() async { - await sut.debug(.all) + @Test + func sendSeveralActions() async { await sut.send(.increment) await sut.send(.increment) await sut.send(.twice) @@ -59,24 +54,18 @@ final class StoreTests: XCTestCase { await sut.expect(\.text, "") } - func test_lotsOfActions() async { + @Test + func lotsOfActions() async { let iterations: Int = 100_000 await sut.send(.incrementMany) await sut.expect(\.count, iterations, timeout: 10) } - func test_threadSafeSendingActions() async { + @Test + func threadSafeSendingActions() async { let iterations: Int = 100_000 let sut = sut! - DispatchQueue.concurrentPerform( - iterations: iterations / 2, - execute: { _ in - Task.detached { - await sut.send(.increment) - } - } - ) - for _ in 0 ..< iterations / 2 { + for _ in 0 ..< iterations { Task.detached { await sut.send(.increment) } @@ -85,18 +74,24 @@ final class StoreTests: XCTestCase { await sut.expect(\.count, iterations) } - func test_asyncAction() async { + @Test + func asyncAction() async { await sut.send(.request) await sut.expect(\.text, "Success") } #if canImport(Combine) - func test_bind() async { + @Test + func bind() async { + let sut = Store( + reducer: BindTestReducer(), + state: BindTestReducer.State(text: "") + ) var result: Set = [] // https://forums.swift.org/t/how-to-use-combine-publisher-with-swift-concurrency-publisher-values-could-miss-events/67193 Task { - try! await Task.sleep(nanoseconds: NSEC_PER_MSEC) + try! await Task.sleep(for: .milliseconds(1)) testPublisher.text.send("first") testPublisher.number.send(1) testPublisher.text.send("second") @@ -109,11 +104,12 @@ final class StoreTests: XCTestCase { if result.count > 4 { break } } - XCTAssertEqual(result, ["", "first", "1", "second", "2"]) + #expect(result == ["", "first", "1", "second", "2"]) } #endif - func test_removeDuplicates() async { + @Test + func removeDuplicates() async { await sut.send(.response("First")) await sut.send(.response("First")) await sut.send(.response("First")) @@ -130,19 +126,20 @@ final class StoreTests: XCTestCase { } } - XCTAssertEqual(result, ["", "First", "Second", "Third"]) + #expect(result == ["", "First", "Second", "Third"]) } - func test_cancel() async { + @Test + func cancel() async { do { let before = await sut.state.text - XCTAssertEqual(before, "") + #expect(before == "") await sut.send(.longTimeTask) await clock.advance(by: .seconds(200 + 1)) let after = await sut.state.text - XCTAssertEqual(after, "Success") + #expect(after == "Success") } await sut.send(.response("")) @@ -155,11 +152,12 @@ final class StoreTests: XCTestCase { await clock.advance(by: .seconds(100)) let text = await sut.state.text - XCTAssertEqual(text, "") + #expect(text == "") } } - func test_debounce() async { + @Test + func debounce() async { for _ in 0..<5 { await clock.advance(by: .seconds(10)) await sut.send(.debouncedIncrement) @@ -182,7 +180,8 @@ final class StoreTests: XCTestCase { await sut.expect(\.count, 2) } - func test_deboouncedSequence() async { + @Test + func deboouncedSequence() async { for _ in 0..<5 { await clock.advance(by: .seconds(10)) await sut.send(.debouncedSequence) @@ -205,7 +204,8 @@ final class StoreTests: XCTestCase { await sut.expect(\.count, 10) } - func test_throttle() async { + @Test + func throttle() async { await sut.send(.throttledIncrement) await sut.send(.throttledIncrement) await clock.advance(by: .seconds(10)) @@ -219,7 +219,8 @@ final class StoreTests: XCTestCase { await sut.expect(\.count, 2) } - func test_throttle_latest() async { + @Test + func throttle_latest() async { await sut.send(.throttledIncrementLatest) await sut.expect(\.count, 1) @@ -238,7 +239,8 @@ final class StoreTests: XCTestCase { await sut.expect(\.count, 4) } - func test_logging_options() async { + @Test + func logging_options() async { let all = Store( reducer: TestReducer(clock: TestClock()), state: TestReducer.State(count: 0, text: ""), @@ -358,8 +360,26 @@ private struct TestReducer: Reducer { .throttle(id: Throttle.incrementLatest, for: .seconds(100), latest: true) } } +} #if canImport(Combine) +private struct BindTestReducer: Reducer { + enum Action: Sendable { + case response(String) + } + + struct State: Equatable { + var text: String + } + + func reduce(state: inout State, action: Action) -> AnyEffect { + switch action { + case .response(let response): + state.text = response + return .none + } + } + func bind() -> AnyEffect { return .merge( .sequence { send in @@ -374,5 +394,5 @@ private struct TestReducer: Reducer { } ) } -#endif } +#endif diff --git a/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift b/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift deleted file mode 100644 index 8b06a59..0000000 --- a/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// OneWay -// The MIT License (MIT) -// -// Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). -// - -import XCTest - -extension XCTestCase { - @MainActor - func sendableExpectWithMainActor( - compare: @Sendable () async -> Bool, - timeout seconds: UInt64 = 1, - description: String = #function - ) async { - let limit = NSEC_PER_SEC * seconds - let start = DispatchTime.now().uptimeNanoseconds - while true { - guard start.distance(to: DispatchTime.now().uptimeNanoseconds) < limit else { - XCTFail("Exceeded timeout of \(seconds) seconds") - break - } - if await compare() { - XCTAssert(true) - break - } else { - await Task.yield() - } - } - } -} - -#if canImport(Darwin) -#else -let NSEC_PER_SEC: UInt64 = 1_000_000_000 -let NSEC_PER_MSEC: UInt64 = 1_000_000 -#endif diff --git a/Tests/OneWayTests/ViewStoreTests.swift b/Tests/OneWayTests/ViewStoreTests.swift index 7015689..4796256 100644 --- a/Tests/OneWayTests/ViewStoreTests.swift +++ b/Tests/OneWayTests/ViewStoreTests.swift @@ -5,41 +5,34 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // +import Testing import OneWay -import XCTest #if !os(Linux) -final class ViewStoreTests: XCTestCase { - @MainActor +@MainActor +struct ViewStoreTests { private var sut: ViewStore! - @MainActor - override func setUp() async throws { + init() { sut = ViewStore( reducer: TestReducer(), state: TestReducer.State(count: 0) ) } - @MainActor - override func tearDown() async throws { - sut = nil - } - - @MainActor - func test_initialState() async { - XCTAssertEqual(sut.initialState, TestReducer.State(count: 0)) - XCTAssertEqual(sut.state.count, 0) + @Test + func initialState() async { + #expect(self.sut.initialState == TestReducer.State(count: 0)) + #expect(self.sut.state.count == 0) for await state in sut.states { - XCTAssertEqual(state.count, 0) - XCTAssertEqual(Thread.isMainThread, true) + #expect(state.count == 0) break } } - @MainActor - func test_sendSeveralActions() async { + @Test + func sendSeveralActions() async { sut.send(.increment) sut.send(.increment) sut.send(.twice) @@ -52,11 +45,11 @@ final class ViewStoreTests: XCTestCase { } } - XCTAssertEqual(result, [0, 1, 2, 3, 4]) + #expect(result == [0, 1, 2, 3, 4]) } - @MainActor - func test_triggeredState() async { + @Test + func triggeredState() async { actor TestResult { var counts: [Int] = [] var triggeredCounts: [Int] = [] @@ -84,12 +77,36 @@ final class ViewStoreTests: XCTestCase { sut.send(.setTriggeredCount(10)) sut.send(.setTriggeredCount(10)) - await sendableExpectWithMainActor { await result.counts == [0, 0, 0, 0] } - await sendableExpectWithMainActor { await result.triggeredCounts == [0, 10, 10, 10] } + await expect( + result, + expectedCounts: [0, 0, 0, 0], + expectedTriggeredCounts: [0, 10, 10, 10] + ) + + func expect( + _ result: TestResult, + expectedCounts: [Int], + expectedTriggeredCounts: [Int], + timeout: Duration = .seconds(1) + ) async { + let clock = ContinuousClock() + let deadline = clock.now + timeout + while clock.now < deadline { + let counts = await result.counts + let triggeredCounts = await result.triggeredCounts + if counts == expectedCounts && triggeredCounts == expectedTriggeredCounts { + #expect(true) + return + } else { + await Task.yield() + } + } + Issue.record("Exceeded timeout of \(timeout.components.seconds) seconds") + } } - @MainActor - func test_ignoredState() async { + @Test + func ignoredState() async { actor TestResult { var counts: [Int] = [] var ignoredCounts: [Int] = [] @@ -118,12 +135,36 @@ final class ViewStoreTests: XCTestCase { sut.send(.setIgnoredCount(30)) // only initial value - await sendableExpectWithMainActor { await result.counts == [0] } - await sendableExpectWithMainActor { await result.ignoredCounts == [0] } + await expect( + result, + expectedCounts: [0], + expectedIgnoredCounts: [0] + ) + + func expect( + _ result: TestResult, + expectedCounts: [Int], + expectedIgnoredCounts: [Int], + timeout: Duration = .seconds(1) + ) async { + let clock = ContinuousClock() + let deadline = clock.now + timeout + while clock.now < deadline { + let counts = await result.counts + let ignoredCounts = await result.ignoredCounts + if counts == expectedCounts && ignoredCounts == expectedIgnoredCounts { + #expect(true) + return + } else { + await Task.yield() + } + } + Issue.record("Exceeded timeout of \(timeout.components.seconds) seconds") + } } - @MainActor - func test_asyncViewStateSequence() async { + @Test + func asyncViewStateSequence() async { sut.send(.concat) var result: [Int] = [] @@ -132,15 +173,13 @@ final class ViewStoreTests: XCTestCase { if result.count > 4 { break } } - XCTAssertEqual(result, [0, 1, 2, 3, 4]) + #expect(result == [0, 1, 2, 3, 4]) } - @MainActor - func test_asyncViewStateSequenceForMultipleConsumers() async { - let expectation = expectation(description: #function) - + @Test + func asyncViewStateSequenceForMultipleConsumers() async { let sut = sut! - let result = TestResult(expectation, expectedCount: 15) + let result = TestResult(expectedCount: 15) Task { @MainActor in await withTaskGroup(of: Void.self) { group in group.addTask { @@ -161,26 +200,25 @@ final class ViewStoreTests: XCTestCase { } } - try! await Task.sleep(nanoseconds: NSEC_PER_MSEC * 10) + try! await Task.sleep(for: .milliseconds(10)) sut.send(.concat) - await fulfillment(of: [expectation], timeout: 1) + await result.waitForCompletion(timeout: 1) let values = await result.values - XCTAssertEqual( - values.sorted(), - [ - 0, 0, 0, - 1, 1, 1, - 2, 2, 2, - 3, 3, 3, - 4, 4, 4, - ] - ) + let expectation = [ + 0, 0, 0, + 1, 1, 1, + 2, 2, 2, + 3, 3, 3, + 4, 4, 4, + ] + #expect(values.sorted() == expectation) } - func test_logging_options() async { - let _ = await ViewStore( + @Test + func logging_options() { + let _ = ViewStore( reducer: TestReducer(), state: TestReducer.State(count: 0) ) @@ -243,24 +281,35 @@ private struct TestReducer: Reducer { } private actor TestResult { - let expectation: XCTestExpectation + private var continuation: CheckedContinuation? let expectedCount: Int var values: [Int] = [] { didSet { if values.count >= expectedCount { - expectation.fulfill() + continuation?.resume() + continuation = nil } } } var count: Int { values.count } - init(_ expectation: XCTestExpectation, expectedCount: Int) { - self.expectation = expectation + init(expectedCount: Int) { self.expectedCount = expectedCount } func insert(_ value: Int) { values.append(value) } + + func waitForCompletion(timeout: Double) async { + await withCheckedContinuation { continuation in + self.continuation = continuation + Task { + try await Task.sleep(for: .seconds(timeout)) + self.continuation?.resume() + self.continuation = nil + } + } + } } #endif