Skip to content

Commit

Permalink
Separate testing functions into a dedicated testing module (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
DevYeom authored Sep 23, 2024
1 parent 33659bb commit 2ba406b
Show file tree
Hide file tree
Showing 20 changed files with 750 additions and 328 deletions.
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ let package = Package(
name: "OneWay",
targets: ["OneWay"]
),
.library(
name: "OneWayTesting",
targets: ["OneWayTesting"]
),
],
dependencies: [
.package(
Expand All @@ -26,19 +30,27 @@ let package = Package(
targets: [
.target(
name: "OneWay",
dependencies: [],
resources: [.copy("PrivacyInfo.xcprivacy")]
),
.testTarget(
name: "OneWayTests",
dependencies: [
"OneWay",
"OneWayTesting",
.product(
name: "Clocks",
package: "swift-clocks"
),
]
),
.target(
name: "OneWayTesting",
dependencies: ["OneWay"]
),
.testTarget(
name: "OneWayTestingTests",
dependencies: ["OneWayTesting"]
),
]
)

Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,15 @@ struct CountingReducer: Reducer {

### Testing

**OneWay** provides the `expect` and `xctExpect` functions to help you write concise and clear tests. These functions work asynchronously, allowing you to verify if the state updates as expected.
**OneWay** provides the `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify whether the state updates as expected.

#### When using `Testing`
Before using the `expect` function, make sure to import the **OneWayTesting** module.

```swift
import OneWayTesting
```

#### When using Testing

You can use the `expect` function to easily check the state value.

Expand All @@ -281,16 +287,16 @@ func incrementTwice() async {
}
```

#### When using `XCTest`
#### When using XCTest

The `xctExpect` function is used within an XCTest environment to assert the state value.
The `expect` function is used in the same way within the `XCTest` environment.

```swift
func test_incrementTwice() async {
await sut.send(.increment)
await sut.send(.increment)

await sut.xctExpect(\.count, 2)
await sut.expect(\.count, 2)
}
```

Expand Down
24 changes: 15 additions & 9 deletions Sources/OneWay/OneWay.docc/Articles/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ Using OneWay for Unit Testing.

## Overview

**OneWay** provides the `expect` and `xctExpect` functions to help you write concise and clear tests. These functions work asynchronously, allowing you to verify if the state updates as expected.
**OneWay** provides the `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify whether the state updates as expected.

Before using the `expect` function, make sure to import the **OneWayTesting** module.

```swift
import OneWayTesting
```

#### When using Testing

Expand All @@ -22,7 +28,7 @@ func incrementTwice() async {

#### When using XCTest

The `xctExpect` function is used within an XCTest environment to assert the state value.
The `expect` function is used in the same way within the `XCTest` environment.

```swift
func test_incrementTwice() async {
Expand Down Expand Up @@ -56,18 +62,18 @@ func test_incrementTwice() async {
await sut.send(.increment)
await sut.send(.increment)

await sut.xctExpect(\.count, 2, timeout: 5)
await sut.expect(\.count, 2, timeout: 5)
}
```

## Failed Tests
## Diagnosing Issues

When a test fails, the output provides detailed information about the failure, making it easy to diagnose issues. Below are example screenshots showing how a failure appears for both `expect` and `xctExpect` functions.
When a test fails, the output provides detailed information about the failure, making it easier to diagnose the issue. Below are example screenshots showing how a failure appears.

#### Failure with expect
#### When using Testing

![failure with expect](expect-failure.png)
![failure with expect](expect-testing-failure.png)

#### Failure with xctExpect
#### When using XCTest

![failure with xctExpect](xct-expect-failure.png)
![failure with xctExpect](expect-xctest-failure.png)
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
190 changes: 1 addition & 189 deletions Sources/OneWay/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ where R.Action: Sendable, R.State: Sendable & Equatable {
/// state changes
public var states: AsyncStream<State>

package var isIdle: Bool { !isProcessing && tasks.isEmpty }
private let reducer: any Reducer<Action, State>
private let continuation: AsyncStream<State>.Continuation
private var isProcessing: Bool = false
private var isIdle: Bool { !isProcessing && tasks.isEmpty }
private var actionQueue: [Action] = []
private var bindingTask: Task<Void, Never>?
private var tasks: [TaskID: Task<Void, Never>] = [:]
Expand Down Expand Up @@ -150,191 +150,3 @@ private struct EffectIDWrapper: Hashable, @unchecked Sendable {
self.id = id
}
}

#if canImport(Testing) && canImport(CoreFoundation)
import CoreFoundation
import Testing

extension Store {
#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, yielding control to allow other tasks to execute, especially
/// when the store is processing or updating its state.
///
/// - 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.
/// - sourceLocation: The source location for tracking the test location.
public func expect<Property>(
_ keyPath: KeyPath<State, Property> & Sendable,
_ input: Property,
timeout: TimeInterval = 2,
sourceLocation: Testing.SourceLocation = #_sourceLocation
) async where Property: Sendable & Equatable {
var isTimeout = false
let start = CFAbsoluteTimeGetCurrent()
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
while !isIdle {
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
let elapsedTime = CFAbsoluteTimeGetCurrent() - start
if elapsedTime > timeout {
isTimeout = true
break
}
}
let result = state[keyPath: keyPath]
if isTimeout && result != input {
Issue.record("Exceeded timeout of \(timeout) seconds", sourceLocation: sourceLocation)
} else {
#expect(result == input, sourceLocation: sourceLocation)
}
}
#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, yielding control to allow other tasks to execute, especially
/// when the store is processing or updating its state.
///
/// - 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.
/// - sourceLocation: The source location for tracking the test location.
public func expect<Property>(
_ keyPath: KeyPath<State, Property>,
_ input: Property,
timeout: TimeInterval = 2,
sourceLocation: Testing.SourceLocation = #_sourceLocation
) async where Property: Sendable & Equatable {
var isTimeout = false
let start = CFAbsoluteTimeGetCurrent()
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
while !isIdle {
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
let elapsedTime = CFAbsoluteTimeGetCurrent() - start
if elapsedTime > timeout {
isTimeout = true
break
}
}
let result = state[keyPath: keyPath]
if isTimeout && result != input {
Issue.record("Exceeded timeout of \(timeout) seconds", sourceLocation: sourceLocation)
} else {
#expect(result == input, sourceLocation: sourceLocation)
}
}
#endif
}
#endif

#if canImport(XCTest) && canImport(CoreFoundation)
import CoreFoundation
import XCTest

extension Store {
#if swift(>=6)
/// An `XCTest`-specific helper that asynchronously waits for the store to finish processing
/// before comparing a specific property in the state to an expected value. It uses
/// `XCTAssertEqual` to validate that the retrieved value matches the input.
///
/// - 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 (default is the current file).
/// - line: The line number from which the function is called (default is the current line).
public func xctExpect<Property>(
_ keyPath: KeyPath<State, Property> & Sendable,
_ input: Property,
timeout: TimeInterval = 2,
file: StaticString = #filePath,
line: UInt = #line
) async where Property: Sendable & Equatable {
var isTimeout = false
let start = CFAbsoluteTimeGetCurrent()
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
while !isIdle {
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
let elapsedTime = CFAbsoluteTimeGetCurrent() - start
if elapsedTime > timeout {
isTimeout = true
break
}
}
let result = state[keyPath: keyPath]
if isTimeout && result != input {
XCTFail("Exceeded timeout of \(timeout) seconds", file: file, line: line)
} else {
XCTAssertEqual(result, input, file: file, line: line)
}
}
#else
/// An `XCTest`-specific helper that asynchronously waits for the store to finish processing
/// before comparing a specific property in the state to an expected value. It uses
/// `XCTAssertEqual` to validate that the retrieved value matches the input.
///
/// - 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 (default is the current file).
/// - line: The line number from which the function is called (default is the current line).
public func xctExpect<Property>(
_ keyPath: KeyPath<State, Property>,
_ input: Property,
timeout: TimeInterval = 2,
file: StaticString = #filePath,
line: UInt = #line
) async where Property: Sendable & Equatable {
var isTimeout = false
let start = CFAbsoluteTimeGetCurrent()
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
while !isIdle {
await Task.detached(priority: .background) {
await Task.yield()
}.value
await Task.yield()
let elapsedTime = CFAbsoluteTimeGetCurrent() - start
if elapsedTime > timeout {
isTimeout = true
break
}
}
let result = state[keyPath: keyPath]
if isTimeout && result != input {
XCTFail("Exceeded timeout of \(timeout) seconds", file: file, line: line)
} else {
XCTAssertEqual(result, input, file: file, line: line)
}
}
#endif
}
#endif
Loading

0 comments on commit 2ba406b

Please sign in to comment.