Skip to content

Instantly share code, notes, and snippets.

@bocato
Last active April 22, 2021 16:44
Show Gist options
  • Save bocato/4c6d896c1404902bd56cde9887c7306c to your computer and use it in GitHub Desktop.
Save bocato/4c6d896c1404902bd56cde9887c7306c to your computer and use it in GitHub Desktop.
Improving Tests in Swift: Failing Mocks - Scenario
import Foundation
import SwiftUI
import Combine
struct LoginService {
struct LoginRequest {
let email: String, password: String
}
let performLogin: (LoginRequest) -> AnyPublisher<String, Error>
let performLoginWithToken: (_ token: String) -> AnyPublisher<String, Error>
}
typealias SecureStorageKey = String
protocol SecureStorageProtocol {
func retrieveValue<T: Decodable>(for key: SecureStorageKey) -> T?
func store<T: Decodable>(_ value: T, inKey key: SecureStorageKey) throws
}
struct LoginState: Equatable {
var email: String = ""
var password: String = ""
var saveCredentialsEnabled: Bool = false
var errorMessage: String?
}
extension SecureStorageKey {
static let loginToken = "login_token"
}
final class LoginViewModel: ObservableObject {
// MARK: - Dependencies
struct Dependencies {
let loginService: LoginService
let secureStorage: SecureStorageProtocol
}
let dependencies: Dependencies
// MARK: - Properties
@Published var state: LoginState
private(set) var subscriptions: Set<AnyCancellable> = .init()
// MARK: Initialization
init(
dependencies: Dependencies,
initalState: LoginState = .init()
) {
self.dependencies = dependencies
self.state = initalState
}
// MARK: - Public API
func onAppear() {
guard let token: String = dependencies
.secureStorage
.retrieveValue(for: .loginToken)
else { return }
dependencies
.loginService
.performLoginWithToken(token)
.sink { [weak self] completion in
if case let .failure(error) = completion {
self?.state.errorMessage = error.localizedDescription
}
} receiveValue: { [state, dependencies] loginToken in
if state.saveCredentialsEnabled {
try? dependencies
.secureStorage
.store(loginToken, inKey: .loginToken)
}
// Then proceed to Logged Area.
}
.store(in: &subscriptions)
}
func performSignIn() {
let request: LoginService.LoginRequest = .init(
email: state.email,
password: state.password
)
dependencies
.loginService
.performLogin(request)
.sink { [weak self] completion in
if case let .failure(error) = completion {
self?.state.errorMessage = error.localizedDescription
}
} receiveValue: { [dependencies] loginToken in
try? dependencies
.secureStorage
.store(loginToken, inKey: .loginToken)
// Then proceed to Logged Area.
}
.store(in: &subscriptions)
}
}
struct LoginView: View {
// MARK: - Dependencies
@StateObject var viewModel: LoginViewModel
// MARK: - Layout
var body: some View {
VStack {
Spacer()
Text("Login")
.font(.title)
VStack {
TextField(
"example@mail.com",
text: $viewModel.state.email
)
}
VStack {
SecureField(
"xxxxxx",
text: $viewModel.state.password
)
}
Button(
"Sign In",
action: viewModel.performSignIn
)
Toggle(
"Save Credentials",
isOn: $viewModel.state.saveCredentialsEnabled
)
Spacer()
if let errorMessage = viewModel.state.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
}
}
.onAppear(perform: viewModel.onAppear)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment