Skip to content

Instantly share code, notes, and snippets.

@bocato
Last active April 8, 2021 17:49
Show Gist options
  • Save bocato/bfe41dd4f8ac39f29ec96683274b8a56 to your computer and use it in GitHub Desktop.
Save bocato/bfe41dd4f8ac39f29ec96683274b8a56 to your computer and use it in GitHub Desktop.
mvvm_form_4.swift
import Combine
import SwiftUI
import XCTest
final class StateBindingViewModelTests: XCTestCase {
// MARK: - Tests
func test_binding_whenSetterIsCalled_shouldUpdateStateValues() {
// Given
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: "",
intValue: 0
)
)
let textBinding: Binding<String> = sut.binding(\.textValue)
let newValue = "New Value"
// When
textBinding.wrappedValue = newValue
// Then
XCTAssertEqual(sut.state.textValue, newValue)
}
func test_binding_whenGetterIsCalled_shouldReturnStateValues() {
// Given
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: "",
intValue: 123
)
)
// When
let intBinding: Binding<Int> = sut.binding(\.intValue)
// Then
XCTAssertEqual(sut.state.intValue, intBinding.wrappedValue)
}
func test_update_shouldSetTheNewValueOnState() {
// Given
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: "",
intValue: 123
)
)
// When
sut.update(\.textValue, to: "New Value")
// Then
XCTAssertEqual(sut.state.textValue, "New Value")
}
func test_stateWillChange_whenNotOverriden_shouldAlwaysReturnTrue() {
// Given
let initialValue = "Initial Value"
let sut: NonBlockingTestViewModelMock = .init(
initialState: .init(
textValue: initialValue,
intValue: 0
)
)
let textBinding: Binding<String> = sut.binding(\.textValue)
let newValue = "New Value"
// When
textBinding.wrappedValue = newValue
// Then
XCTAssertTrue(sut.onStateChangeCalled)
XCTAssertEqual(sut.onStateChangeKeyPathsPassed.last, \TestState.textValue)
XCTAssertEqual(sut.state.textValue, newValue)
}
func test_stateWillChange_shouldAlwaysBeCalled() {
// Given
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: "",
intValue: 0
)
)
let textBinding: Binding<String> = sut.binding(\.textValue)
let newValue = "New Value"
// When
textBinding.wrappedValue = newValue
// Then
XCTAssertTrue(sut.stateWillChangeValueCalled)
XCTAssertEqual(sut.stateWillChangeValueKeyPathsPassed.last, \TestState.textValue)
XCTAssertEqual(sut.stateWillChangeValueNewValuesPassed.last as? String, newValue)
}
func test_onStateChange_whenStateWillChangeIsFalse_itShouldNotBeCalled_andTheStateShouldNotBeUpdated() {
// Given
let initialValue = "Initial Value"
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: initialValue,
intValue: 0
)
)
let newValue = "New Value"
sut.stateWillChangeValueBooleanToBeReturned = false
let textBinding: Binding<String> = sut.binding(\.textValue)
// When
textBinding.wrappedValue = newValue
// Then
XCTAssertTrue(sut.stateWillChangeValueCalled)
XCTAssertEqual(sut.stateWillChangeValueNewValuesPassed.last as? String, newValue)
XCTAssertFalse(sut.onStateChangeCalled, "`onStateChange` should not be called.")
XCTAssertNotEqual(sut.state.textValue, newValue, "The `textValue` should not change to the `newValue`.")
XCTAssertEqual(sut.state.textValue, initialValue, "The `textValue` should still be the `initialValue`.")
}
func test_onStateChange_whenStateWillChangeIsTrue_itShouldBeCalled_andTheStateShouldBeUpdated() {
let initialValue = 1
let sut: TestViewModelMock<TestState> = .init(
initialState: .init(
textValue: "",
intValue: initialValue
)
)
sut.stateWillChangeValueBooleanToBeReturned = true
let intBinding: Binding<Int> = sut.binding(\.intValue)
let newValue = 2
// When
intBinding.wrappedValue = newValue
// Then
XCTAssertTrue(sut.onStateChangeCalled)
XCTAssertEqual(sut.onStateChangeKeyPathsPassed.last, \TestState.intValue)
XCTAssertEqual(sut.state.intValue, newValue)
}
}
// MARK: - Test Doubles and Helpers
struct TestState: Equatable {
var textValue: String
var intValue: Int
}
final class TestViewModelMock<StateType: Equatable>: StateBindingViewModel<StateType> {
private(set) var stateWillChangeValueCallCount = 0
var stateWillChangeValueCalled: Bool { stateWillChangeValueCallCount > 0 }
private(set) var stateWillChangeValueKeyPathsPassed: [PartialKeyPath<StateType>] = []
private(set) var stateWillChangeValueNewValuesPassed: [Any] = []
var stateWillChangeValueBooleansToBeReturned: [Bool] = []
var stateWillChangeValueBooleanToBeReturned = true
override func stateWillChangeValue<Value>(
_ keyPath: PartialKeyPath<StateType>,
newValue: Value
) -> Bool where Value: Equatable {
stateWillChangeValueCallCount += 1
stateWillChangeValueKeyPathsPassed.append(keyPath)
stateWillChangeValueNewValuesPassed.append(newValue)
return stateWillChangeValueBooleansToBeReturned.isEmpty ?
stateWillChangeValueBooleanToBeReturned :
stateWillChangeValueBooleansToBeReturned[stateWillChangeValueCallCount]
}
private(set) var onStateChangeCallCount = 0
var onStateChangeCalled: Bool { onStateChangeCallCount > 0 }
private(set) var onStateChangeKeyPathsPassed: [PartialKeyPath<StateType>] = []
override func onStateChange(_ keyPath: PartialKeyPath<StateType>) {
onStateChangeCallCount += 1
onStateChangeKeyPathsPassed.append(keyPath)
}
}
final class NonBlockingTestViewModelMock: StateBindingViewModel<TestState> {
private(set) var onStateChangeCallCount = 0
var onStateChangeCalled: Bool { onStateChangeCallCount > 0 }
private(set) var onStateChangeKeyPathsPassed: [PartialKeyPath<TestState>] = []
override func onStateChange(_ keyPath: PartialKeyPath<TestState>) {
onStateChangeCallCount += 1
onStateChangeKeyPathsPassed.append(keyPath)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment