From e841e64038db577e1a87bb1f1edb5ed60a9c7912 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 4 Mar 2025 12:20:36 +0100 Subject: [PATCH 1/3] Create CardScannerViewModelTests --- Adyen.xcodeproj/project.pbxproj | 149 +++++++++++++++++- .../AdyenCardScannerTests.swift | 16 -- .../CardScannerViewModelTests.swift | 106 +++++++++++++ .../Mocks/CaptureSessionManagingMock.swift | 72 +++++++++ .../Mocks/CardImageParsingMock.swift | 29 ++++ 5 files changed, 355 insertions(+), 17 deletions(-) delete mode 100644 AdyenCardScannerTests/AdyenCardScannerTests.swift create mode 100644 AdyenCardScannerTests/CardScannerViewModelTests.swift create mode 100644 AdyenCardScannerTests/Mocks/CaptureSessionManagingMock.swift create mode 100644 AdyenCardScannerTests/Mocks/CardImageParsingMock.swift diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index 256716a2c9..ec02774214 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -500,6 +500,8 @@ C9037F162D5A1AC0003A5154 /* CardScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9037F152D5A1AC0003A5154 /* CardScannerViewController.swift */; }; C90D26A727565750001A488F /* FormViewController+ViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90D26A627565750001A488F /* FormViewController+ViewProtocol.swift */; }; C9126051275A3A2800C03DC4 /* BACSItemsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9126050275A3A2800C03DC4 /* BACSItemsFactoryTests.swift */; }; + C91BF2582D771667001F19DE /* CardImageParsingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91BF2572D771667001F19DE /* CardImageParsingMock.swift */; }; + C91BF25A2D7716D4001F19DE /* CaptureSessionManagingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */; }; C92665362C49346200D3D852 /* SubmittableComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92665352C49346200D3D852 /* SubmittableComponent.swift */; }; C927083827590B7500D15EA0 /* BACSInputFormViewProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90D26B1275900B9001A488F /* BACSInputFormViewProtocolMock.swift */; }; C927083927590B7900D15EA0 /* BACSInputPresenterProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90D26B32759048C001A488F /* BACSInputPresenterProtocolMock.swift */; }; @@ -522,6 +524,8 @@ C95903DE275A48D000E7D3BC /* BACSDirectDebitComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95903DD275A48D000E7D3BC /* BACSDirectDebitComponentTests.swift */; }; C96688BF26A6FC1C00DC7297 /* AffirmComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96688BE26A6FC1C00DC7297 /* AffirmComponentTests.swift */; }; C9680BE62D6CAD590076B93D /* CardScannerAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9680BE52D6CAD590076B93D /* CardScannerAssembler.swift */; }; + C96844892D7712DC001DB7F1 /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; }; + C96844932D771328001DB7F1 /* CardScannerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96844922D771328001DB7F1 /* CardScannerViewModelTests.swift */; }; C96E07A3283B92E300345732 /* BACSDirectDebitComponentTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96E07A1283B92D500345732 /* BACSDirectDebitComponentTrackerTests.swift */; }; C978F5EF2732CD6A00F59B3C /* FormCardSecurityCodeItemViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C978F5EE2732CD6A00F59B3C /* FormCardSecurityCodeItemViewTests.swift */; }; C981254E2851E9AF006D1374 /* PaymentComponentSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C981254C2851E903006D1374 /* PaymentComponentSubject.swift */; }; @@ -1270,6 +1274,13 @@ remoteGlobalIDString = E2C0E03222097917008616F6; remoteInfo = Adyen; }; + C968448A2D7712DC001DB7F1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2C0E02A22097917008616F6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; + remoteInfo = AdyenCardScanner; + }; C9FC2C432D50D0B700C9D0F3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E2C0E02A22097917008616F6 /* Project object */; @@ -1855,6 +1866,8 @@ C90D26B32759048C001A488F /* BACSInputPresenterProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSInputPresenterProtocolMock.swift; sourceTree = ""; }; C90D26B5275905B8001A488F /* BACSItemsFactoryProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSItemsFactoryProtocolMock.swift; sourceTree = ""; }; C9126050275A3A2800C03DC4 /* BACSItemsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSItemsFactoryTests.swift; sourceTree = ""; }; + C91BF2572D771667001F19DE /* CardImageParsingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageParsingMock.swift; sourceTree = ""; }; + C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSessionManagingMock.swift; sourceTree = ""; }; C92665352C49346200D3D852 /* SubmittableComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmittableComponent.swift; sourceTree = ""; }; C927083D27590BDB00D15EA0 /* BACSInputFormViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSInputFormViewControllerTests.swift; sourceTree = ""; }; C927083F27590FE800D15EA0 /* BACSInputPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSInputPresenterTests.swift; sourceTree = ""; }; @@ -1874,6 +1887,8 @@ C95C89312BF63A3500C47296 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C96688BE26A6FC1C00DC7297 /* AffirmComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffirmComponentTests.swift; sourceTree = ""; }; C9680BE52D6CAD590076B93D /* CardScannerAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerAssembler.swift; sourceTree = ""; }; + C96844852D7712DC001DB7F1 /* AdyenCardScannerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AdyenCardScannerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C96844922D771328001DB7F1 /* CardScannerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerViewModelTests.swift; sourceTree = ""; }; C96E07A1283B92D500345732 /* BACSDirectDebitComponentTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSDirectDebitComponentTrackerTests.swift; sourceTree = ""; }; C978F5EE2732CD6A00F59B3C /* FormCardSecurityCodeItemViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormCardSecurityCodeItemViewTests.swift; sourceTree = ""; }; C97C16A928059A5A00534419 /* AnalyticsEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventTests.swift; sourceTree = ""; }; @@ -2563,6 +2578,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C96844822D7712DC001DB7F1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C96844892D7712DC001DB7F1 /* AdyenCardScanner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C9FC2C3A2D50D0B700C9D0F3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -3431,6 +3454,15 @@ path = Factories; sourceTree = ""; }; + C91BF2562D7715D8001F19DE /* Mocks */ = { + isa = PBXGroup; + children = ( + C91BF2572D771667001F19DE /* CardImageParsingMock.swift */, + C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; C927083B27590B9100D15EA0 /* Scenes */ = { isa = PBXGroup; children = ( @@ -3528,6 +3560,15 @@ path = Affirm; sourceTree = ""; }; + C96844902D7712ED001DB7F1 /* AdyenCardScannerTests */ = { + isa = PBXGroup; + children = ( + C91BF2562D7715D8001F19DE /* Mocks */, + C96844922D771328001DB7F1 /* CardScannerViewModelTests.swift */, + ); + path = AdyenCardScannerTests; + sourceTree = ""; + }; C96C400E27CCC94500CDBAB8 /* Analytics */ = { isa = PBXGroup; children = ( @@ -4015,6 +4056,7 @@ C99FF2B92D50E872007179C5 /* AdyenCardScanner */, E7E58BF725ADDF8400E869F9 /* Demo */, 81B28F092BCFCE6900CDFA70 /* Tests */, + C96844902D7712ED001DB7F1 /* AdyenCardScannerTests */, E2C0E03422097917008616F6 /* Products */, E2D12C01221ECBB000EF682F /* Frameworks */, 210CC97D2A5FC11900F8F672 /* Recovered References */, @@ -4043,6 +4085,7 @@ 81DF942E2BAAD8EB0001DCBC /* GenerateSnapshots.xctest */, B62D48AC2BBE8D79001EF01A /* UnitTests.xctest */, C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */, + C96844852D7712DC001DB7F1 /* AdyenCardScannerTests.xctest */, ); name = Products; sourceTree = ""; @@ -6055,6 +6098,26 @@ productReference = B62D48AC2BBE8D79001EF01A /* UnitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + C96844842D7712DC001DB7F1 /* AdyenCardScannerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C968448E2D7712DC001DB7F1 /* Build configuration list for PBXNativeTarget "AdyenCardScannerTests" */; + buildPhases = ( + C96844812D7712DC001DB7F1 /* Sources */, + C96844822D7712DC001DB7F1 /* Frameworks */, + C96844832D7712DC001DB7F1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C968448B2D7712DC001DB7F1 /* PBXTargetDependency */, + ); + name = AdyenCardScannerTests; + packageProductDependencies = ( + ); + productName = AdyenCardScannerTests; + productReference = C96844852D7712DC001DB7F1 /* AdyenCardScannerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */ = { isa = PBXNativeTarget; buildConfigurationList = C9FC2C4A2D50D0B800C9D0F3 /* Build configuration list for PBXNativeTarget "AdyenCardScanner" */; @@ -6403,7 +6466,7 @@ E2C0E02A22097917008616F6 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1610; LastUpgradeCheck = 1420; ORGANIZATIONNAME = Adyen; TargetAttributes = { @@ -6416,6 +6479,9 @@ B62D48AB2BBE8D79001EF01A = { CreatedOnToolsVersion = 15.2; }; + C96844842D7712DC001DB7F1 = { + CreatedOnToolsVersion = 16.1; + }; C9FC2C3C2D50D0B700C9D0F3 = { CreatedOnToolsVersion = 16.1; }; @@ -6539,6 +6605,7 @@ E2C0E03B22097917008616F6 /* IntegrationUIKitTests */, F9CCA3B4296ECB3E00AD643D /* SnapshotsTests */, 81DF94042BAAD8EB0001DCBC /* GenerateSnapshots */, + C96844842D7712DC001DB7F1 /* AdyenCardScannerTests */, ); }; /* End PBXProject section */ @@ -6572,6 +6639,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C96844832D7712DC001DB7F1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C9FC2C3B2D50D0B700C9D0F3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -6958,6 +7032,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C96844812D7712DC001DB7F1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C91BF2582D771667001F19DE /* CardImageParsingMock.swift in Sources */, + C91BF25A2D7716D4001F19DE /* CaptureSessionManagingMock.swift in Sources */, + C96844932D771328001DB7F1 /* CardScannerViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C9FC2C392D50D0B700C9D0F3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -8054,6 +8138,11 @@ target = E2C0E03222097917008616F6 /* Adyen */; targetProxy = B6EE0F472BBECBEF00B9810D /* PBXContainerItemProxy */; }; + C968448B2D7712DC001DB7F1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; + targetProxy = C968448A2D7712DC001DB7F1 /* PBXContainerItemProxy */; + }; C9FC2C442D50D0B700C9D0F3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; @@ -8514,6 +8603,55 @@ }; name = Release; }; + C968448C2D7712DC001DB7F1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B2NYSS5932; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.adyen.AdyenCardScanner.AdyenCardScannerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C968448D2D7712DC001DB7F1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B2NYSS5932; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.adyen.AdyenCardScanner.AdyenCardScannerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; C9FC2C472D50D0B700C9D0F3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -9626,6 +9764,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + C968448E2D7712DC001DB7F1 /* Build configuration list for PBXNativeTarget "AdyenCardScannerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C968448C2D7712DC001DB7F1 /* Debug */, + C968448D2D7712DC001DB7F1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C9FC2C4A2D50D0B800C9D0F3 /* Build configuration list for PBXNativeTarget "AdyenCardScanner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AdyenCardScannerTests/AdyenCardScannerTests.swift b/AdyenCardScannerTests/AdyenCardScannerTests.swift deleted file mode 100644 index 449b001a27..0000000000 --- a/AdyenCardScannerTests/AdyenCardScannerTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) 2025 Adyen N.V. -// -// This file is open source and available under the MIT license. See the LICENSE file for more info. -// - -@testable import AdyenCardScanner -import Testing - -struct AdyenCardScannerTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/AdyenCardScannerTests/CardScannerViewModelTests.swift b/AdyenCardScannerTests/CardScannerViewModelTests.swift new file mode 100644 index 0000000000..21e81e0468 --- /dev/null +++ b/AdyenCardScannerTests/CardScannerViewModelTests.swift @@ -0,0 +1,106 @@ +// +// Copyright (c) 2025 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import XCTest +import AVFoundation +@testable import AdyenCardScanner + +final class CardScannerViewModelTests: XCTestCase { + + var cardImageParser: CardImageParsingMock! + var captureSessionManager: CaptureSessionManagingMock! + var sut: CardScannerViewModel! + + override func tearDownWithError() throws { + cardImageParser = nil + captureSessionManager = nil + sut = nil + try super.tearDownWithError() + } + + func testVideoPreviewLayer() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + + let expectedVideoPreviewLayer = AVCaptureVideoPreviewLayer() + captureSessionManager.videoPreviewLayer = expectedVideoPreviewLayer + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + let receivedVideoPreviewLayer = sut.videoPreviewLayer + + // Then + XCTAssertTrue(expectedVideoPreviewLayer === receivedVideoPreviewLayer) + } + + func testConfigureSessionShouldCallCaptureSessionManagerConfigureSession() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + sut.configureSession() + + // Then + XCTAssertEqual(captureSessionManager.configureSessionCallsCount, 1) + } + + func testStartCaptureSessionShouldCallCaptureSessionManagerStartCaptureSession() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + sut.startCaptureSession() + + // Then + XCTAssertEqual(captureSessionManager.startCaptureSessionCallsCount, 1) + } + + func testStartCaptureSessionShouldCallCaptureSessionManagerStopCaptureSession() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + sut.stopCaptureSession() + + // Then + XCTAssertEqual(captureSessionManager.stopCaptureSessionCallsCount, 1) + } + + func testStartCaptureSessionShouldCallCaptureSessionManagerUpdateVideoOrientation() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + sut.updateVideoOrientation() + + // Then + XCTAssertEqual(captureSessionManager.updateVideoOrientationCallsCount, 1) + } +} diff --git a/AdyenCardScannerTests/Mocks/CaptureSessionManagingMock.swift b/AdyenCardScannerTests/Mocks/CaptureSessionManagingMock.swift new file mode 100644 index 0000000000..1e7b15c8a3 --- /dev/null +++ b/AdyenCardScannerTests/Mocks/CaptureSessionManagingMock.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) 2025 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import Foundation +import AVFoundation +@testable import AdyenCardScanner + +class CaptureSessionManagingMock: CaptureSessionManaging { + + // MARK: - delegate + + var delegate: CaptureSessionDelegate? + + // MARK: - videoPreviewLayer + + var videoPreviewLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer() + + // MARK: - configureSession + + var configureSessionCallsCount = 0 + var configureSessionCalled: Bool { + configureSessionCallsCount > 0 + } + var configureSessionClosure: (() -> Void)? + + func configureSession() { + configureSessionCallsCount += 1 + configureSessionClosure?() + } + + // MARK: - startCaptureSession + + var startCaptureSessionCallsCount = 0 + var startCaptureSessionCalled: Bool { + startCaptureSessionCallsCount > 0 + } + var startCaptureSessionClosure: (() -> Void)? + + func startCaptureSession() { + startCaptureSessionCallsCount += 1 + startCaptureSessionClosure?() + } + + // MARK: - stopCaptureSession + + var stopCaptureSessionCallsCount = 0 + var stopCaptureSessionCalled: Bool { + stopCaptureSessionCallsCount > 0 + } + var stopCaptureSessionClosure: (() -> Void)? + + func stopCaptureSession() { + stopCaptureSessionCallsCount += 1 + stopCaptureSessionClosure?() + } + + // MARK: - updateVideoOrientation + + var updateVideoOrientationCallsCount = 0 + var updateVideoOrientationCalled: Bool { + updateVideoOrientationCallsCount > 0 + } + var updateVideoOrientationClosure: (() -> Void)? + + func updateVideoOrientation() { + updateVideoOrientationCallsCount += 1 + updateVideoOrientationClosure?() + } +} diff --git a/AdyenCardScannerTests/Mocks/CardImageParsingMock.swift b/AdyenCardScannerTests/Mocks/CardImageParsingMock.swift new file mode 100644 index 0000000000..1f4c2b192d --- /dev/null +++ b/AdyenCardScannerTests/Mocks/CardImageParsingMock.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + + +import CoreImage +@testable import AdyenCardScanner + +class CardImageParsingMock: CardImageParsing { + // MARK: - parse + + var parseCallsCount = 0 + var parseCalled: Bool { + parseCallsCount > 0 + } + + var parseReceivedImage: CIImage? + var parseReceivedInvocations: [CIImage] = [] + var parseClosure: ((CIImage, @escaping (CreditCard) -> Void) -> Void)? + + func parse(image: CIImage, completion: @escaping (CreditCard) -> Void) { + parseCallsCount += 1 + parseReceivedImage = image + parseReceivedInvocations.append(image) + parseClosure?(image, completion) + } +} From 9502f7aa43143476932a609a2be3327a1886521a Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Wed, 5 Mar 2025 10:23:58 +0100 Subject: [PATCH 2/3] Test didCapture on view model --- Adyen.xcodeproj/project.pbxproj | 4 +++ .../Assets.xcassets/Contents.json | 6 ++++ .../Contents.json | 21 ++++++++++++ .../adyen-card-iphone-capture.jpg | Bin 0 -> 27652 bytes .../CardScannerViewModelTests.swift | 31 ++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 AdyenCardScannerTests/Assets.xcassets/Contents.json create mode 100644 AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/Contents.json create mode 100644 AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/adyen-card-iphone-capture.jpg diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index ec02774214..3d45060865 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -502,6 +502,7 @@ C9126051275A3A2800C03DC4 /* BACSItemsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9126050275A3A2800C03DC4 /* BACSItemsFactoryTests.swift */; }; C91BF2582D771667001F19DE /* CardImageParsingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91BF2572D771667001F19DE /* CardImageParsingMock.swift */; }; C91BF25A2D7716D4001F19DE /* CaptureSessionManagingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */; }; + C91BF25C2D772233001F19DE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C91BF25B2D772233001F19DE /* Assets.xcassets */; }; C92665362C49346200D3D852 /* SubmittableComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92665352C49346200D3D852 /* SubmittableComponent.swift */; }; C927083827590B7500D15EA0 /* BACSInputFormViewProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90D26B1275900B9001A488F /* BACSInputFormViewProtocolMock.swift */; }; C927083927590B7900D15EA0 /* BACSInputPresenterProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90D26B32759048C001A488F /* BACSInputPresenterProtocolMock.swift */; }; @@ -1868,6 +1869,7 @@ C9126050275A3A2800C03DC4 /* BACSItemsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSItemsFactoryTests.swift; sourceTree = ""; }; C91BF2572D771667001F19DE /* CardImageParsingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageParsingMock.swift; sourceTree = ""; }; C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSessionManagingMock.swift; sourceTree = ""; }; + C91BF25B2D772233001F19DE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C92665352C49346200D3D852 /* SubmittableComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmittableComponent.swift; sourceTree = ""; }; C927083D27590BDB00D15EA0 /* BACSInputFormViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSInputFormViewControllerTests.swift; sourceTree = ""; }; C927083F27590FE800D15EA0 /* BACSInputPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSInputPresenterTests.swift; sourceTree = ""; }; @@ -3565,6 +3567,7 @@ children = ( C91BF2562D7715D8001F19DE /* Mocks */, C96844922D771328001DB7F1 /* CardScannerViewModelTests.swift */, + C91BF25B2D772233001F19DE /* Assets.xcassets */, ); path = AdyenCardScannerTests; sourceTree = ""; @@ -6643,6 +6646,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C91BF25C2D772233001F19DE /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AdyenCardScannerTests/Assets.xcassets/Contents.json b/AdyenCardScannerTests/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/AdyenCardScannerTests/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/Contents.json b/AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/Contents.json new file mode 100644 index 0000000000..cc909be615 --- /dev/null +++ b/AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "adyen-card-iphone-capture.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/adyen-card-iphone-capture.jpg b/AdyenCardScannerTests/Assets.xcassets/adyen-card-iphone-capture.imageset/adyen-card-iphone-capture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7e9b04f27ba53cb3be4e317ec6f863dd12f039b GIT binary patch literal 27652 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 NQ{K~oIQUKWe*wwNf=mDa literal 0 HcmV?d00001 diff --git a/AdyenCardScannerTests/CardScannerViewModelTests.swift b/AdyenCardScannerTests/CardScannerViewModelTests.swift index 21e81e0468..c59951c508 100644 --- a/AdyenCardScannerTests/CardScannerViewModelTests.swift +++ b/AdyenCardScannerTests/CardScannerViewModelTests.swift @@ -103,4 +103,35 @@ final class CardScannerViewModelTests: XCTestCase { // Then XCTAssertEqual(captureSessionManager.updateVideoOrientationCallsCount, 1) } + + func testDidCaptureImageShouldParseCardImage() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + let image = UIImage( + named: "adyen-card-iphone-capture", + in: Bundle(for: type(of: self)), + compatibleWith: nil + ) + let cgImage = try XCTUnwrap(image?.cgImage) + let ciImage = CIImage(cgImage: cgImage) + + let expectation = expectation(description: "Image should be parsed") + + cardImageParser.parseClosure = { _, _ in + expectation.fulfill() + } + + // When + sut.didCapture(image: ciImage) + + // Then + waitForExpectations(timeout: 1.0) + XCTAssertEqual(cardImageParser.parseCallsCount, 1) + } } From 9d0c0e4620075c6f82cb3f44b3f10797b190c5c4 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Thu, 6 Mar 2025 14:25:01 +0100 Subject: [PATCH 3/3] Test card image cropping logic --- .../Sources/CardScannerViewModel.swift | 8 +- .../CardScannerViewModelTests.swift | 74 ++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/AdyenCardScanner/Sources/CardScannerViewModel.swift b/AdyenCardScanner/Sources/CardScannerViewModel.swift index b3264e5c3b..961e9c747b 100644 --- a/AdyenCardScanner/Sources/CardScannerViewModel.swift +++ b/AdyenCardScanner/Sources/CardScannerViewModel.swift @@ -75,13 +75,11 @@ class CardScannerViewModel: CardScannerViewModelProtocol { roiInPreviewFrame: CGRect, previewFrame: CGRect ) { - guard let croppedImage = cropRegionOfInterest( + let croppedImage = cropRegionOfInterest( from: image, roiInPreviewFrame: roiInPreviewFrame, previewFrame: previewFrame - ) else { - return - } + ) cardImageParser.parse(image: croppedImage) { creditCard in self.completion(.success(creditCard)) @@ -92,7 +90,7 @@ class CardScannerViewModel: CardScannerViewModelProtocol { from image: CIImage, roiInPreviewFrame: CGRect, previewFrame: CGRect - ) -> CIImage? { + ) -> CIImage { // Scale factors between CIImage and preview frame let imageWidth = image.extent.width let imageHeight = image.extent.height diff --git a/AdyenCardScannerTests/CardScannerViewModelTests.swift b/AdyenCardScannerTests/CardScannerViewModelTests.swift index c59951c508..fae8ef9f3f 100644 --- a/AdyenCardScannerTests/CardScannerViewModelTests.swift +++ b/AdyenCardScannerTests/CardScannerViewModelTests.swift @@ -10,6 +10,10 @@ import AVFoundation final class CardScannerViewModelTests: XCTestCase { + enum Constants { + static let mockCardImage = "adyen-card-iphone-capture" + } + var cardImageParser: CardImageParsingMock! var captureSessionManager: CaptureSessionManagingMock! var sut: CardScannerViewModel! @@ -104,7 +108,7 @@ final class CardScannerViewModelTests: XCTestCase { XCTAssertEqual(captureSessionManager.updateVideoOrientationCallsCount, 1) } - func testDidCaptureImageShouldParseCardImage() throws { + func testDidCaptureWithImageShouldParseCardImage() throws { // Given cardImageParser = CardImageParsingMock() captureSessionManager = CaptureSessionManagingMock() @@ -114,7 +118,7 @@ final class CardScannerViewModelTests: XCTestCase { ) { _ in } let image = UIImage( - named: "adyen-card-iphone-capture", + named: Constants.mockCardImage, in: Bundle(for: type(of: self)), compatibleWith: nil ) @@ -134,4 +138,70 @@ final class CardScannerViewModelTests: XCTestCase { waitForExpectations(timeout: 1.0) XCTAssertEqual(cardImageParser.parseCallsCount, 1) } + + func testDidCaptureWithNilImageShouldNotParseCardImage() throws { + // Given + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + // When + sut.didCapture(image: nil) + + // Then + XCTAssertEqual(cardImageParser.parseCallsCount, 0) + } + + func testDidCaptureWithImageShouldCropImageToRegionOfInterest() throws { + // Given + let expectedCroppedImageSize = CGSize(width: 885.0, height: 1044.0) + + let previewLayerFrame = UIScreen.main.bounds + + let roiInPreviewLayer = CGRect( + x: 20, + y: 200, + width: previewLayerFrame.width - 32, + height: previewLayerFrame.height * 0.5 + ) + + cardImageParser = CardImageParsingMock() + captureSessionManager = CaptureSessionManagingMock() + sut = CardScannerViewModel( + cardImageParser: cardImageParser, + captureSessionManager: captureSessionManager + ) { _ in } + + let image = UIImage( + named: Constants.mockCardImage, + in: Bundle(for: type(of: self)), + compatibleWith: nil + ) + let cgImage = try XCTUnwrap(image?.cgImage) + let originalImage = CIImage(cgImage: cgImage) + + let expectation = expectation(description: "Image should be parsed") + + cardImageParser.parseClosure = { _, _ in + expectation.fulfill() + } + + let cardScannerViewController = CardScannerViewController(viewModel: sut) + cardScannerViewController.viewDidLoad() + + // When + sut.update(previewLayerFrame: previewLayerFrame, roiInPreviewLayer: roiInPreviewLayer) + sut.didCapture(image: originalImage) + + // Then + waitForExpectations(timeout: 1.0) + + let croppedImage = try XCTUnwrap(cardImageParser.parseReceivedImage) + + XCTAssertNotEqual(croppedImage.extent.size, originalImage.extent.size) + XCTAssertEqual(expectedCroppedImageSize, croppedImage.extent.size) + } }