Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guides #75

Merged
merged 19 commits into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ documentation: "Documentation/Guides/*.md"
abstract: "Documentation/Abstracts/*.md"

custom_categories:
- name: Guides
children:
- Testing Reducers
- Testing Selectors
- Testing Effects
- name: Store
children:
- Store
Expand Down
54 changes: 27 additions & 27 deletions Documentation/Abstracts/Test Support.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Every part of an application using Fluxor is highly testable. The separation of the `Action` (instructions), `Selectors` (reading), `Reducer` (mutating) and `Effect` (asynchronous) make each part decoupled, testable and easier to grasp.
Every part of an application using Fluxor is highly testable. The separation of the `Action` (instructions), `Selector` (reading), `Reducer` (mutating) and `Effect` (asynchronous) make each part decoupled, testable and easier to grasp.

But to help out when testing components using Fluxor or asynchronous `Effects`, Fluxor comes with a separate package (**FluxorTestSupport**) with a `MockStore` and a runner for running `Effect` syncronously.
But to help out when testing components using Fluxor or asynchronous `Effect`s, Fluxor comes with a separate package (**FluxorTestSupport**) with a `MockStore`, `TestInterceptor` and an `EffectRunner` to make `Effect`s run syncronously.

FluxorTestSupport should only be linked in unit testing targets.

Expand All @@ -17,32 +17,32 @@ import FluxorTestSupport
import XCTest

class GreetingView: XCTestCase {
func testGreeting() {
let mockStore = MockStore(initialState: AppState())
let view = GreetingView(store: mockStore)
XCTAssert(...)
mockStore.setState(AppState(greeting: "Hi Bob!"))
XCTAssert(...)
}
func testGreeting() {
let mockStore = MockStore(initialState: AppState())
let view = GreetingView(store: mockStore)
XCTAssert(...)
mockStore.setState(AppState(greeting: "Hi Bob!"))
XCTAssert(...)
}
}
```

### Overriding `Selectors`

The `MockStore` can be used to override `Selectors` so that they always return a specific value.
The `MockStore` can be used to override `Selector`s so that they always return a specific value.

```swift
import FluxorTestSupport
import XCTest

class GreetingView: XCTestCase {
func testGreeting() {
let greeting = "Hi Bob!"
let mockStore = MockStore(initialState: AppState(greeting: "Hi Steve!"))
mockStore.overrideSelector(Selectors.getGreeting, value: greeting)
let view = GreetingView(store: mockStore)
XCTAssertEqual(view.greeting, greeting)
}
func testGreeting() {
let greeting = "Hi Bob!"
let mockStore = MockStore(initialState: AppState(greeting: "Hi Steve!"))
mockStore.overrideSelector(Selectors.getGreeting, value: greeting)
let view = GreetingView(store: mockStore)
XCTAssertEqual(view.greeting, greeting)
}
}
```

Expand All @@ -55,15 +55,15 @@ import FluxorTestSupport
import XCTest

class GreetingView: XCTestCase {
func testGreeting() {
let testInterceptor = TestInterceptor<AppState>()
let store = Store(initialState: AppState())
store.register(interceptor: self.testInterceptor)
let view = GreetingView(store: store)
XCTAssertEqual(testInteceptor.stateChanges.count, 0)
view.updateGreeting()
XCTAssertEqual(testInteceptor.stateChanges.count, 1)
}
func testGreeting() {
let testInterceptor = TestInterceptor<AppState>()
let store = Store(initialState: AppState())
store.register(interceptor: self.testInterceptor)
let view = GreetingView(store: store)
XCTAssertEqual(testInteceptor.stateChanges.count, 0)
view.updateGreeting()
XCTAssertEqual(testInteceptor.stateChanges.count, 1)
}
}
```

Expand All @@ -85,7 +85,7 @@ class SettingsEffectsTests: XCTestCase {
func testSetBackground() {
let effects = SettingsEffects()
let action = Actions.setBackgroundColor(payload: .red)
let result = try EffectRunner.run(effects.setBackgroundColor, with: action, environment: AppEnvironment())!
let result = try EffectRunner.run(effects.setBackgroundColor, with: action)!
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result[0], Actions.hideColorPicker())
}
Expand Down
22 changes: 22 additions & 0 deletions Documentation/Guides/Testing Effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Testing `Effect`s

In Fluxor `Effect`s are `Publisher`s based on the actions dispatched in the `Store`. They are inherently asynchronous, so in order to test it in a synchronous test some boilerplate code is needed.

Fluxor comes with an `EffectRunner` (in the FluxorTestSupport package), which will run the `Effect` with a specified `Action` and waits for a given number of expected `Action`s published by the `Effect`.

```swift
import FluxorTestSupport
import XCTest

class SettingsEffectsTests: XCTestCase {
func testSetBackground() {
let effects = SettingsEffects()
let action = Actions.setBackgroundColor(payload: .red)
let result = try EffectRunner.run(effects.setBackgroundColor, with: action)!
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result[0], Actions.hideColorPicker())
}
}
```

To read more about the `EffectRunner`, take a look at the documentation for [FluxorTestSupport](https://fluxor.dev/Test%20Support.html).
31 changes: 31 additions & 0 deletions Documentation/Guides/Testing Reducers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Testing `Reducer`s

In Fluxor `Reducer`s are basically pure functions which takes an instance of `State` and an `Action`, and returns a new `State`.
This means that given the same parameters, the `Reducer` will always return the same output.

```swift
let appReducer = Reducer<CounterState>(
ReduceOn(IncrementAction.self) { state, action in
state.counter += action.value
}
)

class ReducersTests: XCTestCase {
func testIncrementAction() {
// Given
var state = CounterState(counter: 0)
// When
appReducer.reduce(&state, IncrementAction(value: 1))
// Then
XCTAssertEqual(state.counter, 1)
}
}

struct CounterState {
var counter: Int
}

struct IncrementAction: Action {
let value: Int
}
```
43 changes: 43 additions & 0 deletions Documentation/Guides/Testing Selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Testing `Selector`s

In Fluxor `Selector`s are projectors of `State`. `Selector`s can be created by a `KeyPath`, by a closure or based on up to 5 other `Selector`s.
When a `Selector` is based on other `Selector`s, the projector takes the `Value`s from the others as parameters.

## Testing basic `Selector`s

The `Selector`'s `map` function takes the `State` and returns a `Value`.

```swift
struct Selectors {
static let getNameState = Selector(keyPath: \AppState.name)
}

class SelectorsTests: XCTestCase {
func testGetNameState() {
// Given
let state = AppState(name: NameState(firstName: "Tim", lastName: "Cook"))
// Then
XCTAssertEqual(Selectors.getNameState.map(state), state.name)
}
}
```

## Testing `Selector`s based on `Selector`s

If a `Selector` is based on the `Value`s from other `Selector`s, it will also have a `projector` property.
The `projector` can be used in tests, to easily test the `Selector` without creating the full `State` instance.

```swift
extension Selectors {
static let congratulations = Selector.with(getFullName, getBirthday) { fullName, birthday in
"Congratulations \(fullName)! Today is \(birthday.month) \(birthday.day) - your birthday!"
}
}

class SelectorsTests: XCTestCase {
func testCongratulations() {
XCTAssertEqual(Selectors.congratulations.projector("Tim Cook", Birthday(month: "November", day: "1")),
"Congratulations Tim Cook! Today is November 1 - your birthday!")
}
}
```
32 changes: 26 additions & 6 deletions Sources/FluxorTestSupport/EffectRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import Dispatch
import Fluxor
import XCTest

/// A helper for running `Effect`s in tests.
public struct EffectRunner {
/// The `EffectRunner` can be used to run `Effect`s with a specified `Action`.
public struct EffectRunner<Environment> {
/**
Run the `Effect` with the specified `Action` and return the published `Action`s.

Expand All @@ -24,10 +24,10 @@ public struct EffectRunner {
- Returns: The `Action`s published by the `Effect` if it is dispatching
*/
@discardableResult
public static func run<Environment>(_ effect: Effect<Environment>,
with action: Action,
environment: Environment,
expectedCount: Int = 1) throws -> [Action]? {
public static func run(_ effect: Effect<Environment>,
with action: Action,
environment: Environment,
expectedCount: Int = 1) throws -> [Action]? {
let actions = PassthroughSubject<Action, Never>()
let runDispatchingEffect = { (publisher: AnyPublisher<[Action], Never>) throws -> [Action] in
let recorder = ActionRecorder(expectedNumberOfActions: expectedCount)
Expand All @@ -50,6 +50,26 @@ public struct EffectRunner {
}
}

public extension EffectRunner where Environment == Void {
/**
Run the `Effect` with the specified `Action` and return the published `Action`s.

The `expectedCount` defines how many `Action`s the `Publisher` should publish before they are returned.
If the `Effect` is `.nonDispatching`, the `expectedCount` is ignored.

- Parameter effect: The `Effect` to run
- Parameter action: The `Action` to send to the `Effect`
- Parameter expectedCount: The count of `Action`s to wait for
- Returns: The `Action`s published by the `Effect` if it is dispatching
*/
@discardableResult
static func run(_ effect: Effect<Environment>,
with action: Action,
expectedCount: Int = 1) throws -> [Action]? {
return try run(effect, with: action, environment: Void(), expectedCount: expectedCount)
}
}

/**
The `ActionRecorder` records published `Action`s from a `Publisher`.

Expand Down
2 changes: 1 addition & 1 deletion Tests/FluxorTests/EffectsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class EffectsTests: XCTestCase {
}
return publisher.eraseToAnyPublisher()
}
try EffectRunner.run(effect, with: Test1Action(), environment: Void(), expectedCount: 1)
try EffectRunner.run(effect, with: Test1Action(), expectedCount: 1)
XCTAssertNotNil(cancellable)
}
}
Expand Down