42
loading...
This website collects cookies to deliver better user experience
Unidirectional data flow
. And this time we are going to talk about the most important topic - testing. I'd say the whole of this journey was about this topic. This time I’d highly recommend reading all the previous articles, because reading about testing in isolation doesn't make any sense, without understanding the concepts of reactive programming
and Unidirectional data flow
.
cooking saga
, but as I said above and I want to repeat, it will be the most important one. Let's take for the experiments our old friend:enum State {
case initial
case loading
case loaded(data: [String])
}
enum Event {
case dataLoaded(data: [String])
case loadData
}
extension State {
static func reduce(state: State, event: Event) -> State {
var state = state
switch event {
case .dataLoaded(let data):
state = .loaded(data: data)
case .loadData:
state = .loading
}
return state
}
}
reducer
.func reduce(state: State, event: Event) -> State
extension State {
static func reduce(state: State, event: Event) -> State {
var state = state
switch event {
case .dataLoaded(let data):
state = .loaded(data: data)
case .loadData:
state = .loading
}
return state
}
}
func testAfterDataLoadedEventStateWillBeChangedIntoLoadedWithReceivedData() {
// Given
let initialState: State = .initial
let data = ["first", "second"]
// When
let resultState = State.reduce(state: initialState, event: .dataLoaded(data: data))
// Then
XCTAssertEqual(resultState, .loaded(data: data))
}
// User wants to open calendar event edit.
// User doesn't have permission for editing a calendar.
// Request should be sent to the user.
// If the user authorized the permission.
// Calendar event edit will be opened.
// User added new event. -> Calendar State should be saved. And then the scene should be dismissed with Calendar State == nil.
// After dismissing dismissedScene should be calendar.
func testCalendarUserStoryWithPermission() {
// Given
let authorizedEvents = [PermissionType.events: PermissionStatus.authorized]
let emptyPermissions: [PermissionType: PermissionStatus] = [:]
let calendarEvent = CalendarEvent.testEvent
// When
let result = TestStore(
scheduler: scheduler,
disposeBag: disposeBag,
permissionService: { .just(.authorized) }
)
.start(with: [.showScene(.calendar(event: calendarEvent)),
.userFinishedWithCalendarEditor(with: .saved)])
// Then
XCTAssertEqual(result[0].sceneForOpen, nil)
XCTAssertEqual(result[0].permissions, emptyPermissions)
XCTAssertEqual(result[1].sceneForOpen, .calendar(event: calendarEvent))
XCTAssertEqual(result[1].permissions, emptyPermissions)
XCTAssertEqual(result[2].sceneForOpen, .calendar(event: calendarEvent))
XCTAssertEqual(result[2].permissions, authorizedEvents)
XCTAssertEqual(result[3].sceneForOpen, nil)
XCTAssertEqual(result[3].permissions, authorizedEvents)
XCTAssertEqual(result[4].calendarState, .saved)
XCTAssertEqual(result[5].calendarState, nil)
XCTAssertEqual(result[5].dismissedScene, .calendar(result: .saved))
}
TestStore
which is a special test implementation of the store. The biggest difference is that this store can receive events as input for the sake of real user behavior simulation. We have two events from the user right here: showScene
, where the user tries to open a calendar for adding a new event and userFinishedWithCalendarEditor
, whose name speaks for itself.TestStore
I've mocked the permissionService
to return the authorized status for every authorization attempt it helps to test a successful flow.TestStore
and what is the difference with the normal store?import RxCocoa
import RxSwift
import RxTest
open class TestStore<State: Core.State, Event>: Store<State, Event> where State.Event == Event {
private let observer: TestableObserver<State>
private let scheduler: TestScheduler
private let disposeBag: DisposeBag
public init(scheduler: TestScheduler,
disposeBag: DisposeBag,
initial: State,
feedBacks: [SideEffect<State, Event>]) {
self.scheduler = scheduler
self.disposeBag = disposeBag
self.observer = scheduler.createObserver(State.self)
super.init(initial: initial, feedBacks: feedBacks, reducer: State.reduce, scheduler: scheduler)
stateBus
.distinctUntilChanged()
.drive(observer)
.disposed(by: disposeBag)
}
public func start(with events: [Recorded<RxSwift.Event<Event>>] = []) -> [Recorded<RxSwift.Event<State>>] {
scheduler
.createColdObservable(events)
.bind(to: eventBus)
.disposed(by: disposeBag)
scheduler.start()
return observer.events
}
public func start(with events: [Event] = []) -> [State] {
start(
with: events
.enumerated()
.map { .next(($0.offset), $0.element) }
)
.compactMap(\.value.element)
}
}
struct StateStore {
private let scheduler: TestScheduler
init(
scheduler: TestScheduler,
permissionService: (PermissionType) -> Observable<PermissionStatus> = { _ in .never() },
alertModule: (AlertModule.State) -> Observable<GeneralEvent> = { _ in .never() }
)
}
TestScheduler
. This is a special scheduler, which helps us to work with a made-up (virtual) time. If you don’t know what scheduler is, in simple terms every one of your events in the system goes according to the imaginary clock, with this clock ticking and on every tick, an event could appear. So, this clock is the scheduler itself.Composable Architecture
so far... These two fellas really provide a lot of information to think about. While working with the approach that I've shown you before, pointfree.co shows an even more advanced version of testing the whole system. I'll show you a little bit of it, but you can make yourself familiar with this episod which is free to watch.store.assert(
.environment {
$0.currentDate = { 100 }
$0.trainingProcessor = TrainingProcessor(
applyGrade: { _, _, _ in flashCard },
nextTraining: { _ in flashCard }
)
},
.send(.fillTheBlanks(.goNext(grade))),
.receive(.trainingCompleted(.bright)) {
$0.currentScene = .showTheWord(WordSceneState(word: flashCard.word, kind: .correctAnswer))
$0.trainingsRemain = 0
}
)
private let reducer: (inout State, Action, Environment) -> Effect<Action, Never>
send
and receive
. What are they about? The Send
directive is the same as I've demonstrated in the previous approach, it is just what the user or other part of the app could send into your system. Inside a closure of it, you provide all changes of the state which should happen, to validate them. The Receive
directive is the event which you will expect from the system, according to your side effects work. And the same closure to validate the state changes. So here, you really can test the whole flow. assert
function shows you if your state wasn't mutated as expected:🛑 failed - Unexpected state mutation: …
AppState(
todos: [
Todo(
− isComplete: false,
+ isComplete: true,
description: "Milk",
id: 00000000-0000-0000-0000-000000000000
),
]
)
(Expected: −, Actual: +)
Twitter(.atimca)
.subscribe(onNext: { newArcticle in
you.read(newArticle)
})