Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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) } } }

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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) } } }

Slide 6

Slide 6 text

Injeção de dependências?

Slide 7

Slide 7 text

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 }

Slide 8

Slide 8 text

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() } }

Slide 9

Slide 9 text

! Test Driven Design

Slide 10

Slide 10 text

! TDD de fora para dentro

Slide 11

Slide 11 text

! Performance dos testes

Slide 12

Slide 12 text

" Necessidade de mocks

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

?

Slide 15

Slide 15 text

TESTES INTEGRADOS
 SÃO UM GOLPE

Slide 16

Slide 16 text

Caminhos possíveis de execução

Slide 17

Slide 17 text

Tempo de execução da suíte

Slide 18

Slide 18 text

TESTES INTEGRADOS
 SÃO UM GOLPE EXIGEM CUIDADO

Slide 19

Slide 19 text

Como minimizar os testes de integração sem abrir mão de testar as integrações?

Slide 20

Slide 20 text

Usando valores nas fronteiras

Slide 21

Slide 21 text

Actions Store Dispatcher Reducers State Views

Slide 22

Slide 22 text

Views Actions Store Dispatcher Reducers State Views Remote APIs Filesystem Analytics Databases Middleware

Slide 23

Slide 23 text

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) } } }

Slide 24

Slide 24 text

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) } } }

Slide 25

Slide 25 text

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 }

Slide 26

Slide 26 text

func LoginMiddleware() -> Middleware { 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)) } } }}} }

Slide 27

Slide 27 text

// Um nível acima do LoginViewController private let store: Store 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() } } }

Slide 28

Slide 28 text

Views LoginAction Store Dispatcher LoginReducer LoginState LoginViewController Login API Login
 Middleware

Slide 29

Slide 29 text

Reducers são o núcleo da aplicação em termos de regras de negócio

Slide 30

Slide 30 text

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 }

Slide 31

Slide 31 text

+ Caminhos de execução
 + Decisões
 Completamente isolados
 Puramente funcionais
 
 Cenário ideal para testes unitários

Slide 32

Slide 32 text

Middleware são a casca de trás da aplicação

Slide 33

Slide 33 text

func LoginMiddleware() -> Middleware { 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)) } } }}} }

Slide 34

Slide 34 text

- Caminhos de execução
 - Decisões
 + Dependências
 
 Cenário ideal para testes integrados

Slide 35

Slide 35 text

Views são a casca da frente da aplicação

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

- Caminhos de execução
 - Dependências
 + Decisões
 
 Decisões de apresentação podem ser encapsuladas em camadas intermediárias (e. g. view models)

Slide 38

Slide 38 text

Substituir as dependências por valores

Slide 39

Slide 39 text

Testar as interações com valores de entrada

Slide 40

Slide 40 text

Testar a produção de valores de saída

Slide 41

Slide 41 text

Perguntas?

Slide 42

Slide 42 text

Fellipe Caetano
 
 Especialista em desenvolvimento iOS com 7+ de experiência implementando aplicações de nível enterprise para diversos segmentos.
 
 Atua como líder técnico de desenvolvimento iOS na Sympla. [email protected] fellipecaetano fellipecaetano_

Slide 43

Slide 43 text

Obrigado!