From 21806c737f16cbb00077a8d29026e42faf27e636 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Fri, 3 Jun 2022 13:27:18 +0200 Subject: [PATCH] [323] Unit/Integration tests for Home (#335) - integration test for the .onAppear action - unit tests for the synchronizer status change - unit tests for the drawer and tracsation history --- secant.xcodeproj/project.pbxproj | 26 ++- secant/Wrappers/WrappedSDKSynchronizer.swift | 13 +- .../AppTests.swift} | 4 +- secantTests/HomeTests/HomeTests.swift | 217 ++++++++++++++++++ 4 files changed, 245 insertions(+), 15 deletions(-) rename secantTests/{AppReducerTests/AppReducerTests.swift => AppTests/AppTests.swift} (98%) create mode 100644 secantTests/HomeTests/HomeTests.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index c3f43e0..2a72920 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -102,6 +102,7 @@ 9E391124283E4CAC0073DD9A /* ImportWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */; }; 9E391129283F74590073DD9A /* Zatoshi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391128283F74590073DD9A /* Zatoshi.swift */; }; 9E39112E283F91600073DD9A /* ZatoshiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E39112D283F91600073DD9A /* ZatoshiTests.swift */; }; + 9E3911392848AD500073DD9A /* HomeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3911382848AD500073DD9A /* HomeTests.swift */; }; 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; 9E5BF63F2819542C00BA3F17 /* TransactionHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63E2819542C00BA3F17 /* TransactionHistoryTests.swift */; }; @@ -128,7 +129,7 @@ 9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; }; 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */; }; - 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; }; + 9EAFEB822805793200199FC9 /* AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppTests.swift */; }; 9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */; }; 9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */; }; 9EAFEB882806E5AE00199FC9 /* WrappedSDKSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB872806E5AE00199FC9 /* WrappedSDKSynchronizer.swift */; }; @@ -292,6 +293,7 @@ 9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportWalletTests.swift; sourceTree = ""; }; 9E391128283F74590073DD9A /* Zatoshi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zatoshi.swift; sourceTree = ""; }; 9E39112D283F91600073DD9A /* ZatoshiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZatoshiTests.swift; sourceTree = ""; }; + 9E3911382848AD500073DD9A /* HomeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTests.swift; sourceTree = ""; }; 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = ""; }; 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = ""; }; 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionState.swift; sourceTree = ""; }; @@ -318,7 +320,7 @@ 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = ""; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = ""; }; 9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedAudioServices.swift; sourceTree = ""; }; - 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; + 9EAFEB812805793200199FC9 /* AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTests.swift; sourceTree = ""; }; 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = ""; }; 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = ""; }; 9EAFEB872806E5AE00199FC9 /* WrappedSDKSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSDKSynchronizer.swift; sourceTree = ""; }; @@ -478,11 +480,12 @@ 0D4E7A1926B364180058B01E /* secantTests */ = { isa = PBXGroup; children = ( + 9E3911372848AD3A0073DD9A /* HomeTests */, 9E391122283E4C970073DD9A /* ImportWalletTests */, 9E01F8262833CD84000EFC57 /* ScanTests */, 9E5BF642281FEC8700BA3F17 /* SendTests */, 9E5BF63D281953F900BA3F17 /* TransactionHistoryTests */, - 9EAFEB802805791400199FC9 /* AppReducerTests */, + 9EAFEB802805791400199FC9 /* AppTests */, 9EF8135927ECC25E0075AF48 /* UtilTests */, 0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */, 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */, @@ -757,6 +760,14 @@ path = ImportWalletTests; sourceTree = ""; }; + 9E3911372848AD3A0073DD9A /* HomeTests */ = { + isa = PBXGroup; + children = ( + 9E3911382848AD500073DD9A /* HomeTests.swift */, + ); + path = HomeTests; + sourceTree = ""; + }; 9E5BF63D281953F900BA3F17 /* TransactionHistoryTests */ = { isa = PBXGroup; children = ( @@ -931,12 +942,12 @@ path = UIKitBridge; sourceTree = ""; }; - 9EAFEB802805791400199FC9 /* AppReducerTests */ = { + 9EAFEB802805791400199FC9 /* AppTests */ = { isa = PBXGroup; children = ( - 9EAFEB812805793200199FC9 /* AppReducerTests.swift */, + 9EAFEB812805793200199FC9 /* AppTests.swift */, ); - path = AppReducerTests; + path = AppTests; sourceTree = ""; }; 9EAFEB8B2808174900199FC9 /* Sandbox */ = { @@ -1407,11 +1418,12 @@ 9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */, 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, - 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */, + 9EAFEB822805793200199FC9 /* AppTests.swift in Sources */, 9E391124283E4CAC0073DD9A /* ImportWalletTests.swift in Sources */, 9E5BF63F2819542C00BA3F17 /* TransactionHistoryTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, + 9E3911392848AD500073DD9A /* HomeTests.swift in Sources */, 9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */, 9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */, 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */, diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index 5baced7..e43eda0 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -301,11 +301,11 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { func getAllClearedTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false)), - TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2)), - TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true)), - TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4)), - TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5)) + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false), uuid: "1"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2), uuid: "2"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true), uuid: "3"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4), uuid: "4"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5), uuid: "5") ] return Effect( @@ -316,7 +316,8 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { amount: $0.amount, shielded: $0.shielded, status: $0.status, - subtitle: $0.subtitle + subtitle: $0.subtitle, + uuid: $0.uuid ) } ) diff --git a/secantTests/AppReducerTests/AppReducerTests.swift b/secantTests/AppTests/AppTests.swift similarity index 98% rename from secantTests/AppReducerTests/AppReducerTests.swift rename to secantTests/AppTests/AppTests.swift index 02f094c..c11eeaf 100644 --- a/secantTests/AppReducerTests/AppReducerTests.swift +++ b/secantTests/AppTests/AppTests.swift @@ -1,5 +1,5 @@ // -// AppReducerTests.swift +// AppTests.swift // secantTests // // Created by Lukáš Korba on 12.04.2022. @@ -9,7 +9,7 @@ import XCTest @testable import secant_testnet import ComposableArchitecture -class AppReducerTests: XCTestCase { +class AppTests: XCTestCase { static let testScheduler = DispatchQueue.test let testEnvironment = AppEnvironment( diff --git a/secantTests/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift new file mode 100644 index 0000000..0701ce4 --- /dev/null +++ b/secantTests/HomeTests/HomeTests.swift @@ -0,0 +1,217 @@ +// +// HomeTests.swift +// secantTests +// +// Created by Lukáš Korba on 02.06.2022. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture + +class HomeTests: XCTestCase { + func testSynchronizerStateChanged_AnyButSynced() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + walletStorage: .throwing + ) + + let store = TestStore( + initialState: .placeholder, + reducer: HomeReducer.default, + environment: testEnvironment + ) + + store.send(.synchronizerStateChanged(.downloading)) + + store.receive(.updateSynchronizerStatus) + } + + /// When the synchronizer status change to .synced, several things happen + /// 1. the .updateSynchronizerStatus is called + /// 2. the side effect to update the transactions history is called + /// 3. the side effect to update the balance is called + func testSynchronizerStateChanged_Synced() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + walletStorage: .throwing + ) + + let store = TestStore( + initialState: .placeholder, + reducer: HomeReducer.default, + environment: testEnvironment + ) + + store.send(.synchronizerStateChanged(.synced)) + + testScheduler.advance(by: 0.01) + + // ad 1. + store.receive(.updateSynchronizerStatus) + + // ad 2. + let transactionsHelper: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false), uuid: "1"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2), uuid: "2"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true), uuid: "3"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4), uuid: "4"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5), uuid: "5") + ] + let transactions = transactionsHelper.map { + TransactionState.placeholder( + date: Date.init(timeIntervalSince1970: $0.date), + amount: $0.amount, + shielded: $0.shielded, + status: $0.status, + subtitle: $0.subtitle, + uuid: $0.uuid + ) + } + + store.receive(.updateTransactions(transactions)) + + // ad 3. + let balance = Balance(verified: 12_345_000, total: 12_345_000) + + store.receive(.updateBalance(balance)) { state in + state.verifiedBalance = Zatoshi(amount: 12_345_000) + state.totalBalance = Zatoshi(amount: 12_345_000) + } + } + + func testTransactionHistoryPartial_to_FullDrawer() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + walletStorage: .throwing + ) + + let homeState = HomeState( + drawerOverlay: .partial, + profileState: .placeholder, + requestState: .placeholder, + sendState: .placeholder, + scanState: .placeholder, + synchronizerStatus: "", + totalBalance: Zatoshi.zero, + transactionHistoryState: .emptyPlaceHolder, + verifiedBalance: Zatoshi.zero + ) + + let store = TestStore( + initialState: homeState, + reducer: HomeReducer.default, + environment: testEnvironment + ) + + store.send(.transactionHistory(.updateRoute(.all))) { state in + state.transactionHistoryState.route = .all + } + + store.receive(.updateDrawer(.full)) { state in + state.drawerOverlay = .full + state.transactionHistoryState.isScrollable = true + } + } + + func testTransactionHistoryFull_to_PartialDrawer() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + walletStorage: .throwing + ) + + let homeState = HomeState( + drawerOverlay: .full, + profileState: .placeholder, + requestState: .placeholder, + sendState: .placeholder, + scanState: .placeholder, + synchronizerStatus: "", + totalBalance: Zatoshi.zero, + transactionHistoryState: .emptyPlaceHolder, + verifiedBalance: Zatoshi.zero + ) + + let store = TestStore( + initialState: homeState, + reducer: HomeReducer.default, + environment: testEnvironment + ) + + store.send(.transactionHistory(.updateRoute(.latest))) { state in + state.transactionHistoryState.route = .latest + } + + store.receive(.updateDrawer(.partial)) { state in + state.drawerOverlay = .partial + state.transactionHistoryState.isScrollable = false + } + } + + /// The .onAppear action is important to register for the synchronizer state updates. + /// The integration tests make sure registrations and side effects are properly implemented. + func testOnAppear() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + walletStorage: .throwing + ) + + let store = TestStore( + initialState: .placeholder, + reducer: HomeReducer.default, + environment: testEnvironment + ) + + store.send(.onAppear) + + testScheduler.advance(by: 0.01) + + // expected side effects as a result of .onAppear registration + store.receive(.synchronizerStateChanged(.unknown)) + store.receive(.updateSynchronizerStatus) + + // long-living (cancelable) effects need to be properly canceled. + // the .onDisappear action cancles the observer of the synchronizer status change. + store.send(.onDisappear) + } +}