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

Banking on testing

Banking on testing

A glimpse into how we do testing at DNB and our internal data mocking library called Parrot.

rechsteiner

July 01, 2024
Tweet

More Decks by rechsteiner

Other Decks in Technology

Transcript

  1. A bit about our apps • Team of 13 iOS

    developers in total between Spare, Sbanken and DNB mobile bank. • Monorepo with around ~400 000 lines of Swift. • Mature codebases (mobile bank codebase is around 6 years old, Spare a few more)
  2. Move fast and not break things • Many critical features

    that can’t break. • Developers need to be able to con fi dently change code. • It’s important to continuously refactor and improve the codebase. Legacy code is code that you don’t dare to touch. • Automated testing is crucial to maintaining con fi dence in your changes.
  3. A brief intro to Parrot • An internal framework for

    simulating and generating test data. • Roughly speaking, it consists of three parts: • A fake HTTP server • A JSON templating language • Factories 🏭
  4. What we use Parrot for • Faking APIs when developing

    new features. • Reproducing edge-cases when doing manual testing. • Generating models and test data in our unit tests. • Ensuring stable, reproducible UI tests. • Demo mode.
  5. Before Parrot • Each service had a “mock” implementation that

    returned mock data. • Required a lot of development to simulate the same behaviour as the actual services. • Quickly got out of sync with the actual implementation. • Not really testing our services. protocol AccountsServiceType { func fetchAccounts(completion: ((Result<[Account], E -> } final class AccountsService: AccountsServiceType { ... } final class MockAccountsService: AccountsServiceType { func fetchAccounts(completion: ((Result<[Account], E -> completion?(.success([ Account( ... ), Account( ... ), Account( ... ) ])) } }
  6. Faking network requests accounts.json invoices.json balances.json DNB • Allows us

    to fake our APIs on the HTTP level, instead of creating lots of mocks and stubs. • Uses JSON fi les to make it easy to copy the payload from our test environments. • Allows us to control failures, loading times etc. FakeServer
  7. { "data": [ { "accountNumber": "1204XXXXXX5", "accountIdentifier": "5", "providerId": "NO_Sbanken",

    "providerType": "External", "viewType": "EXTERNAL_CARD", "displayBalance": { "value": "200", "currency": "NOK" }, "creditLimit": { "value": "100", "currency": "NOK" }, "bookedBalance": { "value": "100", "currency": "NOK" } } ] } server.on( AccountEndpoint.accounts, response: .template("accounts.json") )
  8. Imports { "data": [ import "payment_card.json", import "payment_card.json", import "payment_card.json"

    { "cardType": "DEBIT", "cardStatus": "NEW_CARD_ORDERED", "lastFourDigits": "0000", "maskedCardNumber": "************0000", "displaySourceProductName": "DNB Kortet", "cardPresentation": "DEFAULT_VISA" } ] }
  9. Variables { "data": { "accountNumber": {{ accountNumber }}, "type": "Brukskonto",

    "alias": {{ alias }}, "openingDate": "2010-01-31", "ownerName": "Ola Nordmann" } } return .template( "account_details.json", variables: [ "accountNumber": "10000", "alias": "Brukskonto" ] )
  10. Request variables { "data": { "id": {{ uri.id }}, "accountNumber":

    {{ body.accountNumber }}, "accountName": {{ body.accountName }} } } POST /accounts/:id { "accountNumber": "1000", "accountName": "Brukskonto" }
  11. Scenarios • Uses the folder structures to represent scenarios. •

    Each folder is a scenario, where each fi le is an override for the default scenario. • Sub-folders can be used to create sub-scenarios. • Great when developing features, fi xing bugs or doing manual testing.
  12. UI testing • Parrot makes our UI tests fast and

    stable. • Simulating speci fi c scenarios in UI tests allows us to cover very speci fi c edge-cases. • By simulating server-side logic, we can make our UI tests more accurate, without the fl akiness of reaching out to our backend systems.
  13. func testCreateBSU() { let app = XCUIApplication(scenario: AppScenario.testsProductCatalogBsu) app.launch() openProductCatalog(app)

    XCTContext.runActivity(named: "Go to BSU consent page") { _ in let openDetailButton = app.buttons["BSU"].firstMatch openDetailButton.tap() let title = app.staticTexts["Home savings scheme for young people"].firstMatch XCTAssertTrue(title.waitForExistence(timeout: 1)) let openButton = app.buttons["Next"].firstMatch XCTAssertTrue(openButton.waitForExistence(timeout: 1)) XCTAssertTrue(openButton.isEnabled) openButton.tap() } UI testing specific scenarios
  14. Simulating server-side logic server.on(Self.deleteDirectDebit(":id")) { request, requestCache in try requestCache.update("agreement_direct_debits.json")

    { directDebits in if let directDebitsArray = directDebits.array { directDebits = .array(directDebitsArray.filter { $0.objectId ?. string != request.uri.id }) } } return .empty() }
  15. Unit testing services • When unit testing services, we prefer

    to have the test data inline instead of using JSON fi les. • We can setup speci fi c responses for a given endpoint. • Clearer when the test data is right there in the test. func testMessagesSuccess() async { let endpoint = InboxEndpoint.messages(id: "0") fakeServer.registerRoute(FakeRoute( path: endpoint.path, method: endpoint.method.rawValue, response: .jsonObject([ "data": [ [ "senderName": "DNB Bank ASA", "subject": "Interest rate change", "body": "Interest rate has changed", "created": "1970-01-30T10:32:23+0200", "attachments": [] ], [ "senderName": "DNB Bank ASA", "subject": "Interest rate change", "body": "Interest rate has changed", "created": "1970-01-30T10:32:23+0200", "attachments": [] ] ] ]) )) let messages = await inboxService.fetchMessages(id: "0")
  16. Factories struct InboxMessageFactory: AttributesFactory { typealias Output = InboxMessage static

    var defaultAttributes = Attributes([ "senderName": "DNB Bank ASA", "subject": "Interest rate change", "body": "Interest rate has changed", "created": "1970-01-30T10:32:23+0200", "attachments": [ InboxMessageAttachmentFactory.attributes() ] ]) .uuid("id") }
  17. func testMessagesSuccess() async { let endpoint = InboxEndpoint.messages(id: "0") fakeServer.registerRoute(FakeRoute(

    path: endpoint.path, method: endpoint.method.rawValue, response: .jsonObject([ "data": [ [ "senderName": "DNB Bank ASA", "subject": "Interest rate change", "body": "Interest rate has changed", "created": "1970-01-30T10:32:23+0200", "attachments": [] ], [ "senderName": "DNB Bank ASA", "subject": "Interest rate change", "body": "Interest rate has changed", "created": "1970-01-30T10:32:23+0200", "attachments": [] ] ] ]) )) let messages = await inboxService.fetchMessages(id: "0") switch messages { case let .success(response): XCTAssertEqual(response.map(\.subject), [ "Message 1", "Message 2" ]) func testMessagesSuccess() async { let endpoint = InboxEndpoint.messages(id: "0") fakeServer.registerRoute(FakeRoute( path: endpoint.path, method: endpoint.method.rawValue, response: .jsonObject([ "data": [ InboxMessageFactory.attributes([ "subject": "Message 1" ]), InboxMessageFactory.attributes([ "subject": "Message 2" ]) ] ]) )) let messages = await inboxService.fetchMessages(id: "0") switch messages { case let .success(response): XCTAssertEqual(response.map(\.subject), [ "Message 1", "Message 2" ])
  18. Creating Swift instances func testShowsEmptyStateWhenNoMessages() throws { let viewModel =

    createViewModel() viewModel.conversationsState = .success([ InboxConversationFactory.build { $0.title = "A" }, InboxConversationFactory.build { $0.title = "B" } ]) let components = viewModel.components() XCTAssertComponent(in: components, be: ListComponent.self) { component in let items = component.items() XCTAssertEqual(items.count, 2) XCTAssertItem(at: 0, in: items, be: InboxConversationListItem.self) { item in XCTAssertEqual(item.conversation.title, "B") }
  19. Creating Swift instances func testShowsEmptyStateWhenNoMessages() throws { let viewModel =

    createViewModel() viewModel.conversationsState = .success([ InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build(), InboxConversationFactory.build() ]) let components = viewModel.components() XCTAssertComponent(in: components, be: ListComponent.self) { component in let items = component.items()
  20. Test data in SwiftUI Previews #Preview { InboxMessageView( message: InboxMessageFactory.build

    { $0.attachments = [ InboxMessageAttachmentFactory.build(), InboxMessageAttachmentFactory.build(), InboxMessageAttachmentFactory.build() ] }, theme: theme, onSelectAttachment: { _ in } ) }
  21. Benefits of factories • Reduces a lot of boilerplate when

    writing tests and previews. • By only overriding speci fi c properties, the tests communicate clearly what the actual requirements are for the test to pass.
  22. protocol Factory { associatedtype Output static var defaultValue: Output {

    get } } extension Factory { static func build(_ callback: (inout Output) -> Void = { _ in }) -> Output { var value = defaultValue callback(&value) return value } }