Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Efeitos colaterais em apps iOS: isolando-os e atingindo máxima testabilidade na prática

Efeitos colaterais em apps iOS: isolando-os e atingindo máxima testabilidade na prática

Código que lida apenas com funções puras e valores imutáveis em vez de objetos complexos pode ser executado com a certeza de que só o que importa são valores de saída em relação a valores de entrada. Este tipo de código oferece máxima testabilidade, mas vem com uma dúvida fundamental: como escrever aplicações reais dessa forma, sem comunicação com a Internet ou sem persistência local?

Nesta palestra vamos conhecer como aplicar conceitos de programação funcional na vida real, isolando completamente o gerenciamento de efeitos colaterais como I/O ou acesso aos sensores do dispositivo e solucionando problemas que existem hoje, deixando a parte impura da aplicação com pouca ou nenhuma lógica.

Fellipe Caetano

August 13, 2018
Tweet

More Decks by Fellipe Caetano

Other Decks in Programming

Transcript

  1. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  2. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  3. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  4. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  5. struct Environment { private static var stack: [Environment] = [.default]

    static var current: Environment { return stack.last! } private static var `default`: Environment { return .init(loginService: LoginService()) } static func push(loginService: LoginServiceProtocol) { stack.append(.init(loginService: loginService)) } @discardableResult static func pop() -> Environment? { return stack.popLast() } let loginService: LoginServiceProtocol }
  6. final class LoginViewControllerTests: XCTestCase { func testLoginInCaseOfSuccess() { let loginService

    = StubLoginService(result: .success(/* ... */)) Environment.push(loginService: loginService) let loginViewController = LoginViewController() XCTAssert(loginViewController.view != nil) loginViewController.loginTextField = /* ... */ loginViewController.passwordTextField = /* ... */ loginViewController.loginButton.sendActions(for: .touchUpInside) XCTAssertFalse(loginViewController.activityIndicator.isAnimating) Environment.pop() } }
  7. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  8. ?

  9. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  10. final class LoginViewController: UIViewController { private let dispatch: (Action) ->

    Void init (dispatch: @escaping (Action) -> Void) { super.init(nibName: nil, bundle: nil) self.dispatch = dispatch } // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): dispatch(LoginAction.perform(login: login, password: password)) case let .failed(error): interpret(validationError: error) } } }
  11. func LoginReducer(state: LoginState, action: LoginAction) -> LoginState { var newState

    = state switch action { case .perform: newState.isPerforming = true case let .success(user): newState.isPerforming = false newState.loggedInUser = user newState.error = nil case let .failure(error): newState.isPerforming = false newState.loggedInUser = nil newState.error = error } return newState }
  12. func LoginMiddleware() -> Middleware<LoginState, LoginAction> { return { getState, dispatch

    in { next in { action in next(action) guard case let LoginAction.perform(login, password) = action else { return } let loginService = Environment.current.loginService loginService.perform(login: login, password: password) { result in switch result { case let .success(user): dispatch(LoginAction.success(user)) case let .failure(error): dispatch(LoginAction.failure(error)) } } }}} }
  13. // Um nível acima do LoginViewController private let store: Store<LoginState,

    LoginAction> private var subscriptionToken: SubscriptionToken? // ... subscriptionToken = store.subscribe { state in loginViewController.render(state: state) } final class LoginViewController: UIViewController { // ... func render(state: LoginState) { if state.isPerforming { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } } }
  14. func LoginReducer(state: LoginState, action: LoginAction) -> LoginState { var newState

    = state switch action { case .perform: newState.isPerforming = true case let .success(user): newState.isPerforming = false newState.loggedInUser = user newState.error = nil case let .failure(error): newState.isPerforming = false newState.loggedInUser = nil newState.error = error } return newState }
  15. func LoginMiddleware() -> Middleware<LoginState, LoginAction> { return { getState, dispatch

    in { next in { action in next(action) guard case let LoginAction.perform(login, password) = action else { return } let loginService = Environment.current.loginService loginService.perform(login: login, password: password) { result in switch result { case let .success(user): dispatch(LoginAction.success(user)) case let .failure(error): dispatch(LoginAction.failure(error)) } } }}} }
  16. - Caminhos de execução
 - Dependências
 + Decisões
 
 Tornam-se

    mais simples de testar usando métodos como testes de UI e snapshots
  17. - Caminhos de execução
 - Dependências
 + Decisões
 
 Decisões

    de apresentação podem ser encapsuladas em camadas intermediárias (e. g. view models)