Last active
April 22, 2021 16:44
-
-
Save bocato/4c6d896c1404902bd56cde9887c7306c to your computer and use it in GitHub Desktop.
Improving Tests in Swift: Failing Mocks - Scenario
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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