Skip to content

Instantly share code, notes, and snippets.

@turekj
Last active January 2, 2020 11:13
Show Gist options
  • Save turekj/7caf745520be5c0724ff45d2132a80a2 to your computer and use it in GitHub Desktop.
Save turekj/7caf745520be5c0724ff45d2132a80a2 to your computer and use it in GitHub Desktop.
Separating Decisions and Side-Effects: Before Refactoring
// 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