diff --git a/Sources/OneWay/OneWay.docc/Articles/Testing.md b/Sources/OneWay/OneWay.docc/Articles/Testing.md index 455bfa4..f9fccc1 100644 --- a/Sources/OneWay/OneWay.docc/Articles/Testing.md +++ b/Sources/OneWay/OneWay.docc/Articles/Testing.md @@ -12,6 +12,19 @@ Before using the `expect` function, make sure to import the **OneWayTesting** mo import OneWayTesting ``` +When testing a reducer, you need to use `Store` instead of `ViewStore` for the `expect` function to be available. + +```swift +let sut = Store( + reducer: TestReducer(), + state: TestReducer.State(count: 0) +) +await sut.send(.increment) +await sut.expect(\.count, 1) +``` + +The completion of `send`'s `await` literally means that `send` has finished. It does not mean that the state has fully changed. The state change always happpens asynchronously. Therefore, tests should be written using the `expect` function. + #### When using Testing You can use the `expect` function to easily check the state value. @@ -35,7 +48,7 @@ func test_incrementTwice() async { await sut.send(.increment) await sut.send(.increment) - await sut.xctExpect(\.count, 2) + await sut.expect(\.count, 2) } ``` diff --git a/Sources/OneWay/Store.swift b/Sources/OneWay/Store.swift index 632faf5..b1e7812 100644 --- a/Sources/OneWay/Store.swift +++ b/Sources/OneWay/Store.swift @@ -40,7 +40,12 @@ where R.Action: Sendable, R.State: Sendable & Equatable { /// state changes. public var states: AsyncStream - package var isIdle: Bool { !isProcessing && tasks.isEmpty } + /// Returns `true` if the store is idle, meaning it's not processing and there are no pending + /// tasks for side effects. + public var isIdle: Bool { + !isProcessing && tasks.isEmpty + } + private let reducer: any Reducer private let continuation: AsyncStream.Continuation private var isProcessing: Bool = false diff --git a/Sources/OneWay/ViewStore.swift b/Sources/OneWay/ViewStore.swift index add4ba5..5fd7c9f 100644 --- a/Sources/OneWay/ViewStore.swift +++ b/Sources/OneWay/ViewStore.swift @@ -41,7 +41,7 @@ where R.Action: Sendable, R.State: Sendable & Equatable { /// state changes public let states: AsyncViewStateSequence - package let store: Store + private let store: Store private let continuation: AsyncStream.Continuation private var task: Task? diff --git a/Sources/OneWayTesting/ViewStore+Testing.swift b/Sources/OneWayTesting/ViewStore+Testing.swift deleted file mode 100644 index e3b19ac..0000000 --- a/Sources/OneWayTesting/ViewStore+Testing.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// OneWay -// The MIT License (MIT) -// -// Copyright (c) 2022-2024 SeungYeop Yeom ( https://github.com/DevYeom ). -// - -import OneWay - -#if !os(Linux) -#if canImport(Testing) -extension ViewStore { - #if swift(>=6) - /// Allows the expectation of a certain property value in the store's state. It compares the - /// current value of the given `keyPath` in the state with an expected `input` value - /// - /// The function works asynchronously, waiting for the store to become idle, i.e., when the - /// store is not actively processing or updating its state, before performing the comparison. - /// - /// - Parameters: - /// - keyPath: A key path that specifies the property in the `State` to be compared. - /// - input: The expected value of the property at the given key path. - /// - timeout: The maximum amount of time (in seconds) to wait for the store to finish - /// processing before timing out. Defaults to 2 seconds. - /// - fileID: The file ID from which the function is called. - /// - filePath: The file path from which the function is called. - /// - line: The line number from which the function is called. - /// - column: The column number from which the function is called. - public func expect( - _ keyPath: KeyPath & Sendable, - _ input: Property, - timeout: Double = 2, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async where Property: Sendable & Equatable { - await Task { @MainActor in - await Task.yield() - }.value - await store.expect( - keyPath, - input, - timeout: timeout, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - #else - /// Allows the expectation of a certain property value in the store's state. It compares the - /// current value of the given `keyPath` in the state with an expected `input` value - /// - /// The function works asynchronously, waiting for the store to become idle, i.e., when the - /// store is not actively processing or updating its state, before performing the comparison. - /// - /// - Parameters: - /// - keyPath: A key path that specifies the property in the `State` to be compared. - /// - input: The expected value of the property at the given key path. - /// - timeout: The maximum amount of time (in seconds) to wait for the store to finish - /// processing before timing out. Defaults to 2 seconds. - /// - fileID: The file ID from which the function is called. - /// - filePath: The file path from which the function is called. - /// - line: The line number from which the function is called. - /// - column: The column number from which the function is called. - public func expect( - _ keyPath: KeyPath, - _ input: Property, - timeout: Double = 2, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async where Property: Sendable & Equatable { - await Task { @MainActor in - await Task.yield() - }.value - await store.expect( - keyPath, - input, - timeout: timeout, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - #endif -} -#endif - -#if !canImport(Testing) && canImport(XCTest) -extension ViewStore { - #if swift(>=6) - /// Allows the expectation of a certain property value in the store's state. It compares the - /// current value of the given `keyPath` in the state with an expected `input` value - /// - /// The function works asynchronously, waiting for the store to become idle, i.e., when the - /// store is not actively processing or updating its state, before performing the comparison. - /// - /// - Parameters: - /// - keyPath: A key path that specifies the property in the `State` to be compared. - /// - input: The expected value of the property at the given key path. - /// - timeout: The maximum amount of time (in seconds) to wait for the store to finish - /// processing before timing out. Defaults to 2 seconds. - /// - file: The file path from which the function is called. - /// - line: The line number from which the function is called. - public func expect( - _ keyPath: KeyPath & Sendable, - _ input: Property, - timeout: Double = 2, - file: StaticString = #filePath, - line: UInt = #line - ) async where Property: Sendable & Equatable { - await Task { @MainActor in - await Task.yield() - }.value - await store.expect( - keyPath, - input, - timeout: timeout, - file: file, - line: line - ) - } - #else - /// Allows the expectation of a certain property value in the store's state. It compares the - /// current value of the given `keyPath` in the state with an expected `input` value - /// - /// The function works asynchronously, waiting for the store to become idle, i.e., when the - /// store is not actively processing or updating its state, before performing the comparison. - /// - /// - Parameters: - /// - keyPath: A key path that specifies the property in the `State` to be compared. - /// - input: The expected value of the property at the given key path. - /// - timeout: The maximum amount of time (in seconds) to wait for the store to finish - /// processing before timing out. Defaults to 2 seconds. - /// - file: The file path from which the function is called. - /// - line: The line number from which the function is called. - public func expect( - _ keyPath: KeyPath, - _ input: Property, - timeout: Double = 2, - file: StaticString = #filePath, - line: UInt = #line - ) async where Property: Sendable & Equatable { - await Task { @MainActor in - await Task.yield() - }.value - await store.expect( - keyPath, - input, - timeout: timeout, - file: file, - line: line - ) - } - #endif -} -#endif -#endif diff --git a/Tests/OneWayTestingTests/TestingTests.swift b/Tests/OneWayTestingTests/TestingTests.swift index 2351201..afc0667 100644 --- a/Tests/OneWayTestingTests/TestingTests.swift +++ b/Tests/OneWayTestingTests/TestingTests.swift @@ -5,7 +5,8 @@ // Copyright (c) 2022-2024 SeungYeop Yeom ( https://github.com/DevYeom ). // -#if canImport(Testing) +#if canImport(Testing) && canImport(Darwin) +import Darwin import Testing import OneWay @@ -38,33 +39,28 @@ struct TestingTests { await store.expect(\.nested.doubleNested.value, 1.23) } - #if !os(Linux) @Test - func viewStoreExpect() async { - let store = await ViewStore( + func storeExpectWithManyActions() async { + let store = Store( reducer: TestReducer(), state: TestReducer.State(count: 0) ) await store.expect(\.count, 0) - await store.send(.increment) - await store.expect(\.count, 1) - - await store.send(.increment) - await store.expect(\.count, 2) - - await store.send(.setName("hello")) - await store.expect(\.nested.name, "hello") + for _ in 0..<10_000 { + await store.send(.increment) + } + await store.expect(\.count, 10_000) - await store.send(.setValue(1.23)) - await store.expect(\.nested.doubleNested.value, 1.23) + await store.send(.delayedIncrement) + await store.expect(\.count, 10_001) } - #endif } private struct TestReducer: Reducer { enum Action: Sendable { case increment + case delayedIncrement case setName(String) case setValue(Double) } @@ -86,6 +82,11 @@ private struct TestReducer: Reducer { case .increment: state.count += 1 return .none + case .delayedIncrement: + return .single { + try! await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + return .increment + } case let .setName(name): state.nested.name = name return .none diff --git a/Tests/OneWayTestingTests/XCTestTests.swift b/Tests/OneWayTestingTests/XCTestTests.swift index 5549a7e..74b7272 100644 --- a/Tests/OneWayTestingTests/XCTestTests.swift +++ b/Tests/OneWayTestingTests/XCTestTests.swift @@ -5,6 +5,8 @@ // Copyright (c) 2022-2024 SeungYeop Yeom ( https://github.com/DevYeom ). // +#if canImport(XCTest) && canImport(Darwin) +import Darwin import XCTest import OneWay @@ -35,32 +37,27 @@ final class XCTestTests: XCTestCase { await store.expect(\.nested.doubleNested.value, 1.23) } - #if !os(Linux) - func test_viewStoreExpect() async { - let store = await ViewStore( + func test_storeExpectWithManyActions() async { + let store = Store( reducer: TestReducer(), state: TestReducer.State(count: 0) ) await store.expect(\.count, 0) - await store.send(.increment) - await store.expect(\.count, 1) - - await store.send(.increment) - await store.expect(\.count, 2) - - await store.send(.setName("hello")) - await store.expect(\.nested.name, "hello") + for _ in 0..<10_000 { + await store.send(.increment) + } + await store.expect(\.count, 10_000) - await store.send(.setValue(1.23)) - await store.expect(\.nested.doubleNested.value, 1.23) + await store.send(.delayedIncrement) + await store.expect(\.count, 10_001) } - #endif } private struct TestReducer: Reducer { enum Action: Sendable { case increment + case delayedIncrement case setName(String) case setValue(Double) } @@ -82,6 +79,11 @@ private struct TestReducer: Reducer { case .increment: state.count += 1 return .none + case .delayedIncrement: + return .single { + try! await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + return .increment + } case let .setName(name): state.nested.name = name return .none @@ -91,3 +93,4 @@ private struct TestReducer: Reducer { } } } +#endif diff --git a/Tests/OneWayTests/ViewStoreTests.swift b/Tests/OneWayTests/ViewStoreTests.swift index ad5d3c0..ba60683 100644 --- a/Tests/OneWayTests/ViewStoreTests.swift +++ b/Tests/OneWayTests/ViewStoreTests.swift @@ -44,7 +44,15 @@ final class ViewStoreTests: XCTestCase { sut.send(.increment) sut.send(.twice) - await sut.expect(\.count, 4) + var result: [Int] = [] + for await state in sut.states { + result.append(state.count) + if result.count > 4 { + break + } + } + + XCTAssertEqual(result, [0, 1, 2, 3, 4]) } @MainActor