Last active
January 2, 2020 11:13
-
-
Save turekj/7caf745520be5c0724ff45d2132a80a2 to your computer and use it in GitHub Desktop.
Separating Decisions and Side-Effects: Before Refactoring
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
// See article at: https://jakubturek.com/separating-decisions-and-effects/ | |
import Foundation | |
import Quick | |
import Nimble | |
// MARK: - Data models | |
struct UserToken: Codable { | |
let token: String | |
enum CodingKeys: String, CodingKey { | |
case token = "session_token" | |
} | |
} | |
struct ValidationErrors: Codable { | |
let errors: [String] | |
enum CodingKeys: String, CodingKey { | |
case errors = "validation_errors" | |
} | |
} | |
struct DummyError: Error, LocalizedError { | |
let errorDescription: String? | |
init(_ errorDescription: String) { | |
self.errorDescription = errorDescription | |
} | |
} | |
// MARK: - Dependencies | |
protocol SimpleService { | |
typealias Completion = (Result<String, Error>) -> Void | |
func call(_ completion: @escaping Completion) | |
} | |
protocol AlertPresenter { | |
func present(error: String) | |
} | |
protocol TokenStore { | |
func set(sessionToken: String) | |
} | |
// MARK: - Spies | |
class SimpleServiceSpy: SimpleService { | |
var callInvoked: SimpleService.Completion? | |
func call(_ completion: @escaping SimpleService.Completion) { | |
callInvoked = completion | |
} | |
} | |
class AlertPresenterSpy: AlertPresenter { | |
var presentInvoked: String? | |
func present(error: String) { | |
presentInvoked = error | |
} | |
} | |
class TokenStoreSpy: TokenStore { | |
var setSessionTokenInvoked: String? | |
func set(sessionToken: String) { | |
setSessionTokenInvoked = sessionToken | |
} | |
} | |
// MARK: - System under the test | |
class SimpleViewController: UIViewController { | |
let service: SimpleService | |
let alertPresenter: AlertPresenter | |
let tokenStore: TokenStore | |
let validationErrorLabel = UILabel() | |
init(service: SimpleService, alertPresenter: AlertPresenter, tokenStore: TokenStore) { | |
self.service = service | |
self.alertPresenter = alertPresenter | |
self.tokenStore = tokenStore | |
super.init(nibName: nil, bundle: nil) | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
service.call { [weak self] in self?.handle(response: $0) } | |
} | |
func decode<Value: Decodable>(from json: String) -> Value? { | |
let data = Data(json.utf8) | |
return try? JSONDecoder().decode(Value.self, from: data) | |
} | |
func handle(response: Result<String, Error>) { | |
if case let .failure(error) = response { | |
showAlert(error: error) | |
} else if case let .success(json) = response, | |
let error: ValidationErrors = decode(from: json) { | |
showValidationError(error) | |
} else if case let .success(json) = response, | |
let token: UserToken = decode(from: json) { | |
storeToken(token) | |
} | |
} | |
private func showAlert(error: Error) { | |
alertPresenter.present(error: error.localizedDescription) | |
} | |
private func showValidationError(_ error: ValidationErrors) { | |
let message = error.errors.joined(separator: "\n") | |
validationErrorLabel.text = message | |
} | |
private func storeToken(_ sessionToken: UserToken) { | |
tokenStore.set(sessionToken: sessionToken.token) | |
} | |
required init?(coder: NSCoder) { nil } | |
} | |
// MARK: - The test method | |
func encodeJSON<Value: Encodable>(_ value: Value) -> String { | |
let data = try! JSONEncoder().encode(value) | |
return String(data: data, encoding: .utf8)! | |
} | |
class SimpleViewControllerSpec: QuickSpec { | |
override func spec() { | |
describe("SimpleViewController") { | |
var service: SimpleServiceSpy! | |
var alertPresenter: AlertPresenterSpy! | |
var tokenStore: TokenStoreSpy! | |
var sut: SimpleViewController! | |
beforeEach { | |
service = SimpleServiceSpy() | |
alertPresenter = AlertPresenterSpy() | |
tokenStore = TokenStoreSpy() | |
sut = SimpleViewController(service: service, alertPresenter: alertPresenter, tokenStore: tokenStore) | |
} | |
afterEach { | |
tokenStore = nil | |
service = nil | |
alertPresenter = nil | |
sut = nil | |
} | |
context("when view is loaded") { | |
beforeEach { | |
_ = sut.view | |
} | |
context("on request error") { | |
beforeEach { | |
service.callInvoked?(.failure(DummyError("You're offline"))) | |
} | |
it("should present error alert") { | |
expect(alertPresenter.presentInvoked).toNot(beNil()) | |
expect(alertPresenter.presentInvoked) == "You're offline" | |
} | |
} | |
context("on validation error") { | |
beforeEach { | |
let error = ValidationErrors(errors: ["Not", "A", "Good", "Data"]) | |
service.callInvoked?(.success(encodeJSON(error))) | |
} | |
it("should update the error label") { | |
expect(sut.validationErrorLabel.text).toNot(beNil()) | |
expect(sut.validationErrorLabel.text) == "Not\nA\nGood\nData" | |
} | |
} | |
context("on session token received") { | |
beforeEach { | |
let token = UserToken(token: "user_session") | |
service.callInvoked?(.success(encodeJSON(token))) | |
} | |
it("should update the session token") { | |
expect(tokenStore.setSessionTokenInvoked).toNot(beNil()) | |
expect(tokenStore.setSessionTokenInvoked) == "user_session" | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment