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

Testando o App do Nubank - TDC Florianópolis 2019

Testando o App do Nubank - TDC Florianópolis 2019

Francesco

April 23, 2019
Tweet

More Decks by Francesco

Other Decks in Programming

Transcript

  1. Testando o App do Nubank Victor Maraccini Mobile Software Engineer

    @vmaraccini | [email protected] Francesco Perrotti-Garcia Mobile Software Engineer @fpg1503 | [email protected]
  2. • Mais de 1200 funcionários • Diversidade • Mais de

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

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

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

    refatorar e fazer mudanças • Novos engenheiros tem confiança nas entregas
  6. 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
  7. struct MGMChannelData { let type: MGMChannel } struct MGMChannelData {

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

    count: Int? let canOpen: Bool } struct MGMChannelData { let count: Int? }
  9. Implementação class MGMInteractor { } struct MGMChannelData { let type:

    MGMChannel let count: Int? let canOpen: Bool } let canOpen: Bool
  10. class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) ->

    MGMChannelData { } } struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } Implementação
  11. class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) ->

    MGMChannelData { switch channel { case .whatsapp: } } } struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } Implementação
  12. class MGMInteractor { ... func createChannelData(invitations: Int, channel: Customer.Channel) ->

    MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) } } } struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } Implementação
  13. 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) ... } } } struct MGMChannelData { let type: MGMChannel let count: Int? let canOpen: Bool } Implementação
  14. 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)
  15. Removendo 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 }
  16. struct MGMChannelData { let type: MGMChannel let count: Int? let

    canOpen: Bool } class MGMInteractor { ... func createChannelData(urlHandler: URLHandler, 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) ... } } } urlHandler: URLHandler, protocol URLHandler { func canOpenURL(_ url: URL) -> Bool } case .whatsapp: let canOpenWhatsapp = UIApplication.shared .canOpenURL(Hypermedia.whatsApp) Injeção de dependências
  17. struct MGMChannelData { let type: MGMChannel let count: Int? let

    canOpen: Bool } class MGMInteractor { ... func createChannelData(urlHandler: URLHandler, invitations: Int, channel: Customer.Channel) -> MGMChannelData { switch channel { case .whatsapp: let canOpenWhatsapp = urlHandler.canOpenURL(Hypermedia.whatsApp) return MGMChannelData(type: .whatsapp, count: nil, canOpen: canOpenWhatsapp) ... } } } urlHandler: URLHandler, protocol URLHandler { func canOpenURL(_ url: URL) -> Bool } case .whatsapp: let canOpenWhatsapp = urlHandler.canOpenURL(Hypermedia.whatsApp) Injeção de dependências
  18. Testes unitários 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 } func testChannelDataCreationWhatsappCannotOpen() { }
  19. Testes unitários func testChannelDataCreationWhatsappCannotOpen() { let urlHandler = URLHandlerMock(canOpenWhatsapp: false)

    let invites = Int.arbitrary.generate let channelData = MGMInteractor.createChannelData(invitations: invites, channel: .whatsapp, urlHandler: urlHandler) } 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 }
  20. Testes unitários 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 }
  21. Testes unitários 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)
  22. Funções puras • Possuí retorno (não é void) • Resultado

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

    determinístico • Dado um input, retorna sempre o mesmo output • Como funções matemáticas: f(x) = y
  24. Funções puras (ou não) func save(identities: [User]) -> Int {

    let db = Database() return db.save(identities) } func squared(n: Int) -> Int { return n * n }
  25. Testes unitários Exemplo: Efeitos colaterais class Controller { init() {

    let viewController = ViewController() ... viewController.action .subscribe(onNext: { MGMManager().invite() }) } } Integração Unitários
  26. = ViewController() init() { = ... .invite() } } viewController.action

    .subscribe(onNext: { }) Testes unitários Dependências implícitas Integração Unitários MGMManager() class Controller { let viewController
  27. viewController.action .subscribe(onNext: { }) Testes unitários Injeção de dependências let

    mgmManager: MGMManager init(mgmManager: viewController: ViewController) { self.mgmManager = mgmManager self.viewController = viewController mgmManager.invite() } } Integração Unitários : ViewController let mgmManager: MGMManager init(mgmManager: viewController: ViewController) { class Controller { let viewController MGMManager()
  28. 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
  29. 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 mgmManager.invite()
  30. Testes unitários Exemplo: Efeitos colaterais func testInviteMGM() { let mgmManager

    = StubMGMManager() let stubViewController = StubViewController() let controller = Controller(mgmManager: mgmManager, viewController: stubViewController) } Integração Unitários
  31. 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(()) } Integração Unitários let stubViewController = StubViewController() viewController: stubViewController //Given a simulated tap stubViewController.confirmSubject.onNext(())
  32. 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
  33. Testes unitários de view Integração Unitários • Permite validar o

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

    layout em várias condições de uso • Uso de screenshots
  35. 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 time de Design
  36. 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 time de Design • Evita modificações não intencionais no layout
  37. 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 time de Design • Evita modificações não intencionais no layout • Permite validar a acessibilidade das telas
  38. Testes unitários de view Integração Unitários • Como testar layout

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

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

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

    • Como testar layout isolado? • Dividir responsabilidades • Desacoplar comportamentos
  42. Testes unitários de view Integração Unitários ViewController Controller View ViewModel

    Repassa Ações Cria • Como testar layout isolado? • Dividir responsabilidades • Desacoplar comportamentos
  43. 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
  44. Testes unitários de view Integração Unitários ViewController Controller View ViewModel

    Repassa Ações Repassa Ações Atualiza Cria Medium: Building Nubank - iOS App Architecture • Como testar layout isolado? • Dividir responsabilidades • Desacoplar comportamentos
  45. 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
  46. Testes unitários de view Integração Unitários Exercitar todos os tamanhos

    da tela ou componente * Telas não necessariamente verdadeiras
  47. 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
  48. 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
  49. Testes unitários de view Integração Unitários Exercitar todos os estados

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

    da tela ou componente * Telas não necessariamente verdadeiras
  51. Testes unitários de view 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())) ] } 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())) ] Exemplo: Teste de estados
  52. Testes unitários de view 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 } Exemplo: Teste de estados
  53. try permutations.forEach { identifier, viewModel in } Testes unitários de

    view 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) Exemplo: Teste de estados
  54. Testes de integração Integração Unitários • Foco no funcionamento dos

    componentes em conjunto • Fluxos reais do app com poucas modificações
  55. 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
  56. 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") } Integração Unitários
  57. 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") } 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
  58. Recapitulando Pirâmide de testes • Muitos testes unitários • Alguns

    testes de integração • Poucos testes end-to-end
  59. Recapitulando Escrevendo código testável • Evitar dependências implícitas • Injeção

    de dependências • Funções puras • Arquitetura desacoplada