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

Testando o App do Nubank - CocoaHeads

Francesco
February 02, 2019

Testando o App do Nubank - CocoaHeads

Francesco

February 02, 2019
Tweet

More Decks by Francesco

Other Decks in Technology

Transcript

  1. • Mais de 1200 funcionários • Diversidade • Mais de

    5MM de clientes • Clientes fazem tudo pelo app O que é Nubank?
  2. • Mais de 1200 funcionários • Diversidade • Mais de

    5MM de clientes • Clientes fazem tudo pelo app • Cultura de qualidade e testes O que é Nubank?
  3. Show of hands Quem aqui: • Acredita que testes melhoram

    a qualidade do código • Escreve testes no dia-a-dia
  4. Por que testamos? • Evitar regressão • Mais velocidade para

    refatorar e fazer mudanças • Novos engenheiros tem confiança nas entregas
  5. Por que testamos? • Evitar regressão • Mais velocidade para

    refatorar e fazer mudanças • Novos engenheiros tem confiança nas entregas • Garantir comportamentos existentes • De forma automatizada • Em diferentes condições de uso
  6. Nossa pirâmide de testes em Mobile Integração Unitários E2E Testes

    antes de cada merge Deploy a cada merge Rollout Teste A/B
  7. Nossa pirâmide de testes em Mobile Integração Unitários E2E Testes

    antes de cada merge Deploy a cada merge Rollout Teste A/B Checklist
  8. struct MGMChannelData { let type: MGMChannel } struct MGMChannelData {

    let type: MGMChannel let count: Int? let canOpen: Bool } Contexto Integração Unitários
  9. Contexto Integração Unitários struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } struct MGMChannelData { let count: Int? }
  10. Contexto Integração Unitários struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } struct MGMChannelData { let canOpen: Bool }
  11. Dependências implícitas struct MGMChannelData { let type: MGMChannel let count:

    Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } }
  12. Dependências implícitas struct MGMChannelData { let type: MGMChannel let count:

    Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp)
  13. Dependências implícitas struct MGMChannelData { let type: MGMChannel let count:

    Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) protocol URLHandler { func canOpenURL(_ url: URL) -> Bool }
  14. Funções puras • Possuí retorno (não é void) • Resultado

    determinístico • Dado um input, retorna sempre o mesmo resultado
  15. Funções puras • Possuí retorno (não é void) • Resultado

    determinístico • Dado um input, retorna sempre o mesmo resultado • Como funções matemáticas: f(x) = y
  16. Funções puras (ou não) func fibonacci(n: UInt) -> UInt {

    guard n > 1 else { return n } return fibonacci(n: n-1) + fibonacci(n: n-2) }
  17. Funções puras (ou não) func save(identities: [User]) -> Int {

    let db = Database() return db.save(identities) } func fibonacci(n: UInt) -> UInt { guard n > 1 else { return n } return fibonacci(n: n-1) + fibonacci(n: n-2) }
  18. Injeção de dependências struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) protocol URLHandler { func canOpenURL(_ url: URL) -> Bool }
  19. Injeção de dependências struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData( invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } protocol URLHandler { func canOpenURL(_ url: URL) -> Bool } func createChannelData(urlHandler: URLHandler,
  20. Injeção de dependências struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData( invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } protocol URLHandler { func canOpenURL(_ url: URL) -> Bool } func createChannelData(urlHandler: URLHandler, let canOpenWhatsapp = UIApplication.shared
  21. Injeção de dependências struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData( invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: .canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } func createChannelData(urlHandler: URLHandler, let canOpenWhatsapp = urlHandler
  22. Injeção de dependências struct MGMChannelData { let type: MGMChannel let

    count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData( invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } protocol URLHandler { func canOpenURL(_ url: URL) -> Bool } func createChannelData(urlHandler: URLHandler, let canOpenWhatsapp = urlHandler.canOpenURL(Hypermedia.whatsApp)
  23. Testes unitários Exemplo: Funções “puras" func testChannelDataCreationWhatsappCannotOpen() { let urlHandler

    = URLHandlerMock(canOpenWhatsapp: false) let invites = Int.arbitrary.generate let channelData = MGMInteractor.createChannelData(invitations: invites, channel: .whatsapp, urlHandler: urlHandler) expect(channelData) == MGMChannelData(type: .whatsapp, count: nil, canOpen: false) } Integração Unitários struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(urlHandler: URLHandler, invitations: Int, channel: Customer.Channel) -> MGMChannelData }
  24. Testes unitários Exemplo: Funções “puras" func testChannelDataCreationWhatsappCannotOpen() { let urlHandler

    = URLHandlerMock(canOpenWhatsapp: false) let invites = Int.arbitrary.generate let channelData = MGMInteractor.createChannelData(invitations: invites, channel: .whatsapp, urlHandler: urlHandler) expect(channelData) == MGMChannelData(type: .whatsapp, count: nil, canOpen: false) } Integração Unitários struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } class MGMInteractor { ... func createChannelData(urlHandler: URLHandler, invitations: Int, channel: Customer.Channel) -> MGMChannelData } canOpen: false) let urlHandler = URLHandlerMock(canOpenWhatsapp: false)
  25. Testes unitários Exemplo: Efeitos colaterais class Controller { init() {

    let viewController = ViewController() ... viewController.action .subscribe(onNext: { MGMManager().invite() }) } } Integração Unitários
  26. Testes unitários Exemplo: Efeitos colaterais class Controller { let viewController:

    ViewController let mgmManager: MGMManager init(mgmManager: MGMManager, viewController: ViewController) { self.mgmManager = mgmManager self.viewController = viewController viewController.action .subscribe(onNext: { mgmManager.invite() }) } } Integração Unitários
  27. Testes unitários Exemplo: Efeitos colaterais func testInviteMGM() { let mgmManager

    = StubMGMManager() let stubViewController = StubViewController() let controller = Controller(mgmManager: mgmManager, viewController: stubViewController) //Given a simulated tap stubViewController.confirmSubject.onNext(()) //Expect side-effects expect(mgmManager.inviteInvocationCount) == 1 } Integração Unitários
  28. Testes unitários Exemplo: Efeitos colaterais func testInviteMGM() { let mgmManager

    = StubMGMManager() let stubViewController = StubViewController() let controller = Controller(mgmManager: mgmManager, viewController: stubViewController) //Given a simulated tap stubViewController.confirmSubject.onNext(()) //Expect side-effects expect(mgmManager.inviteInvocationCount) == 1 } Integração Unitários let stubViewController = StubViewController() viewController: stubViewController //Given a simulated tap stubViewController.confirmSubject.onNext(())
  29. func testInviteMGM() { let mgmManager = StubMGMManager() let stubViewController =

    StubViewController() let controller = Controller(mgmManager: mgmManager, viewController: stubViewController) //Given a simulated tap stubViewController.confirmSubject.onNext(()) //Expect side-effects expect(mgmManager.inviteInvocationCount) == 1 } //Expect side-effects expect(mgmManager.inviteInvocationCount) == 1 let mgmManager = StubMGMManager() mgmManager: mgmManager, Testes unitários Exemplo: Efeitos colaterais Integração Unitários
  30. Testes unitários de view Integração Unitários • Permite validar o

    layout em várias condições de uso • Uso de screenshots
  31. Testes unitários de view Integração Unitários • Permite validar o

    layout em várias condições de uso • Uso de screenshots
  32. Testes unitários de view Integração Unitários • Permite validar o

    layout em várias condições de uso • Uso de screenshots • Facilita a verificação com o Design
  33. Testes unitários de view Integração Unitários • Permite validar o

    layout em várias condições de uso • Uso de screenshots • Facilita a verificação com o Design • Evita modificações não intencionais no layout
  34. Testes unitários de view Integração Unitários • Permite validar o

    layout em várias condições de uso • Uso de screenshots • Facilita a verificação com o Design • Evita modificações não intencionais no layout • Permite validar a acessibilidade das telas
  35. Testes unitários de view Integração Unitários • Como testar layout

    isolado? • Dividir responsabilidades • Desacoplar comportamentos
  36. Testes unitários de view Integração Unitários • Como testar layout

    isolado? • Dividir responsabilidades • Desacoplar comportamentos Tela
  37. Testes unitários de view Integração Unitários ViewController Controller View ViewModel

    Repassa Ações Repassa Ações Atualiza Cria • Como testar layout isolado? • Dividir responsabilidades • Desacoplar comportamentos Medium: Building Nubank - iOS App Architecture
  38. Testes unitários de view Integração Unitários ViewController Controller View ViewModel

    Repassa Ações Repassa Ações Atualiza Cria • Como testar layout isolado? • Dividir responsabilidades • Desacoplar comportamentos Medium: Building Nubank - iOS App Architecture
  39. Testes unitários de view Integração Unitários • Como testar layout

    isolado? • Dividir responsabilidades • Desacoplar comportamentos ViewController Controller View ViewModel Repassa Ações Repassa Ações Atualiza Cria Medium: Building Nubank - iOS App Architecture
  40. Testes unitários de view Integração Unitários • Como testar layout

    isolado? • Dividir responsabilidades • Desacoplar comportamentos ViewController Controller View ViewModel Repassa Ações Repassa Ações Atualiza Cria Medium: Building Nubank - iOS App Architecture
  41. Testes unitários de view Integração Unitários Exercitar todos os tamanhos

    da tela ou componente * Telas não necessariamente verdadeiras
  42. Testes unitários de view Integração Unitários Exercitar todos os tamanhos

    da tela ou componente * Telas não necessariamente verdadeiras 4S 6/6S/7/8 6P/7P/8P 5/5S/5C/SE X
  43. Testes unitários de view Integração Unitários Exercitar todos os tamanhos

    de fonte na tela ou componente CategoryXS CategoryXXXL CategoryAccessibilityXXXL * Telas não necessariamente verdadeiras
  44. Testes unitários de view Integração Unitários Exercitar todos os estados

    da tela ou componente * Telas não necessariamente verdadeiras
  45. Testes unitários de view Integração Unitários Exercitar todos os estados

    da tela ou componente * Telas não necessariamente verdadeiras
  46. Testes unitários de view Exemplo func testLayout() throws { let

    permutations = [ ("popup", PopupViewModel(popup: somePopup)), ("one-action", PopupViewModel(popup: oneActionPopup)), ("no-icon", PopupViewModel(popup: noIconPopup)), ("recognized-timeout", .recognizedTimeout(url: .empty())), ("unrecognized-block", .unrecognizedBlock(url: .empty())) ] try permutations.forEach { identifier, viewModel in let viewController = PopupViewController() viewController.bind(viewModel) try assertView(viewController.view, width: 350, identifier: identifier, screenshotService: service) } } Integração Unitários
  47. Testes unitários de view Exemplo func testLayout() throws { let

    permutations = [ ("popup", PopupViewModel(popup: somePopup)), ("one-action", PopupViewModel(popup: oneActionPopup)), ("no-icon", PopupViewModel(popup: noIconPopup)), ("recognized-timeout", .recognizedTimeout(url: .empty())), ("unrecognized-block", .unrecognizedBlock(url: .empty())) ] try permutations.forEach { identifier, viewModel in let viewController = PopupViewController() viewController.bind(viewModel) try assertView(viewController.view, width: 350, identifier: identifier, screenshotService: service) } } Integração Unitários let permutations = [ ("popup", PopupViewModel(popup: somePopup)), ("one-action", PopupViewModel(popup: oneActionPopup)), ("no-icon", PopupViewModel(popup: noIconPopup)), ("recognized-timeout", .recognizedTimeout(url: .empty())), ("unrecognized-block", .unrecognizedBlock(url: .empty())) ]
  48. Testes unitários de view Exemplo func testLayout() throws { let

    permutations = [ ("popup", PopupViewModel(popup: somePopup)), ("one-action", PopupViewModel(popup: oneActionPopup)), ("no-icon", PopupViewModel(popup: noIconPopup)), ("recognized-timeout", .recognizedTimeout(url: .empty())), ("unrecognized-block", .unrecognizedBlock(url: .empty())) ] try permutations.forEach { identifier, viewModel in let viewController = PopupViewController() viewController.bind(viewModel) try assertView(viewController.view, width: 350, identifier: identifier, screenshotService: service) } } Integração Unitários let viewController = PopupViewController() viewController.bind(viewModel) try permutations.forEach { identifier, viewModel in }
  49. Testes unitários de view Exemplo func testLayout() throws { let

    permutations = [ ("popup", PopupViewModel(popup: somePopup)), ("one-action", PopupViewModel(popup: oneActionPopup)), ("no-icon", PopupViewModel(popup: noIconPopup)), ("recognized-timeout", .recognizedTimeout(url: .empty())), ("unrecognized-block", .unrecognizedBlock(url: .empty())) ] try permutations.forEach { identifier, viewModel in let viewController = PopupViewController() viewController.bind(viewModel) try assertView(viewController.view, width: 350, identifier: identifier, screenshotService: service) } } Integração Unitários try assertView(viewController.view, width: 350, identifier: identifier, screenshotService: service) try permutations.forEach { identifier, viewModel in }
  50. Testes de integração Integração Unitários • Foco no funcionamento dos

    componentes em conjunto • Fluxos reais do app com poucas modificações
  51. Testes de integração Integração Unitários • Foco no funcionamento dos

    componentes em conjunto • Fluxos reais do app com poucas modificações • Requests mockados internamente no teste
  52. Testes de integração Exemplo func login() { tester().tapView(withAccessibilityLabel: "LOGIN") tester().waitForSoftwareKeyboard()

    tester().enterText(intoCurrentFirstResponder: validCPF) tester().tapView(withAccessibilityLabel: "CONTINUAR") tester().enterText(intoCurrentFirstResponder: validPassword) tester().tapView(withAccessibilityLabel: "ENTRAR") } func testReorderFlow() throws { tester().swipeView(withAccessibilityIdentifier: "global-actions", inDirection: .left) tester().moveRow(at: IndexPath(row: 0, section: 0), to: IndexPath(row: 1, section: 0), in: tester().firstTableView()) tester().waitForAnimationsToFinish() tester().tapView(withAccessibilityLabel: "Voltar") tester().swipeView(withAccessibilityIdentifier: "global-actions", inDirection: .right) tester().waitForAnimationsToFinish() } Integração Unitários
  53. Testes de integração Exemplo func login() { tester().tapView(withAccessibilityLabel: "LOGIN") tester().waitForSoftwareKeyboard()

    tester().enterText(intoCurrentFirstResponder: validCPF) tester().tapView(withAccessibilityLabel: "CONTINUAR") tester().enterText(intoCurrentFirstResponder: validPassword) tester().tapView(withAccessibilityLabel: "ENTRAR") } func testReorderFlow() throws { tester().swipeView(withAccessibilityIdentifier: "global-actions", inDirection: .left) tester().moveRow(at: IndexPath(row: 0, section: 0), to: IndexPath(row: 1, section: 0), in: tester().firstTableView()) tester().waitForAnimationsToFinish() tester().tapView(withAccessibilityLabel: "Voltar") tester().swipeView(withAccessibilityIdentifier: "global-actions", inDirection: .right) tester().waitForAnimationsToFinish() } Integração Unitários tapView waitForSoftwareKeyboard enterText swipeView moveRow waitForAnimationsToFinish
  54. Estatísticas no App Nativo iOS 0 1.500.000 3.000.000 4.500.000 6.000.000

    0 175.000 350.000 525.000 700.000 06/2016 12/2016 06/2017 12/2017 06/2018 12/2018 LOC Clientes
  55. Estatísticas no App Nativo iOS 0 1.500.000 3.000.000 4.500.000 6.000.000

    0 175.000 350.000 525.000 700.000 06/2016 12/2016 06/2017 12/2017 06/2018 12/2018 LOC Clientes 426.839 472.620 581.691 626.059 358.513 409.269
  56. Estatísticas no App Nativo iOS 0 1.500.000 3.000.000 4.500.000 6.000.000

    0 175.000 350.000 525.000 700.000 06/2016 12/2016 06/2017 12/2017 06/2018 12/2018 LOC Clientes 426.839 472.620 581.691 626.059 358.513 409.269 Rescrita de componentes e novo app
  57. 0 2.000 4.000 6.000 8.000 0 175.000 350.000 525.000 700.000

    06/2016 12/2016 06/2017 12/2017 06/2018 12/2018 LOC Testes 426.839 472.620 581.691 626.059 358.513 409.269 Rescrita de componentes e novo app Estatísticas no App Nativo iOS
  58. Estatísticas 0 10 20 30 06/2016 12/2016 06/2017 12/2017 06/2018

    12/2018 Frameworks Engenheiros Mobile Reestruturação em Frameworks