Slide 1

Slide 1 text

Martin Rechsteiner Banking on testing How we do testing at DNB

Slide 2

Slide 2 text

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)

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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 🏭

Slide 6

Slide 6 text

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.

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

{ "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") )

Slide 10

Slide 10 text

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" } ] }

Slide 11

Slide 11 text

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" ] )

Slide 12

Slide 12 text

Request variables { "data": { "id": {{ uri.id }}, "accountNumber": {{ body.accountNumber }}, "accountName": {{ body.accountName }} } } POST /accounts/:id { "accountNumber": "1000", "accountName": "Brukskonto" }

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

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.

Slide 15

Slide 15 text

Debug panel

Slide 16

Slide 16 text

UI testing

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

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.

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Unit testing

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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" ])

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Test data in SwiftUI Previews #Preview { InboxMessageView( message: InboxMessageFactory.build { $0.attachments = [ InboxMessageAttachmentFactory.build(), InboxMessageAttachmentFactory.build(), InboxMessageAttachmentFactory.build() ] }, theme: theme, onSelectAttachment: { _ in } ) }

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Demo

Slide 32

Slide 32 text

Thank you! Martin Rechsteiner @rechsteiner