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

Garantindo qualidade no app do Nubank

Francesco
December 08, 2018

Garantindo qualidade no app do Nubank

Francesco

December 08, 2018
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 • Mexeu com Mobile
  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 Refactor interno 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