bcsc-core-native-testing
- Repo stars 0
- Author repo skills-registry
bcsc-core Native Unit Testing
Overview
This skill guides creation of native unit tests for the packages/bcsc-core module, which contains Swift (iOS) and Kotlin (Android) implementations of authentication, PIN management, keychain storage, token services, and device authentication.
The codebase already uses protocol-based dependency injection on iOS and interface/class-based injection on Android, making it straightforward to create mock implementations for isolated unit testing.
Test Infrastructure
iOS — XCTest via SPM
Tests are built with Swift Package Manager using the package manifest at packages/bcsc-core/Package.swift. The key concepts:
BcscCoreTestabletarget: A subset of production iOS source files compiled for the simulator (without React Native / CocoaPods dependencies). Only files listed in thesources:array are included.BcscCoreTeststest target: XCTest files underios/BcscCoreTests/, compiled with@testable import BcscCoreTestable.SPM_BUILDflag: A Swift compiler define set inPackage.swiftviaswiftSettings: [.define("SPM_BUILD")]. The fileios/SPMCompat.swiftuses#if SPM_BUILDto provide lightweight stubs (e.g.,SecureRandom,Data.arrayOfBytes()) for types from JWE files that aren't included in the SPM target.- Scheme name: xcodebuild uses
BcscCoreTests-Package(SPM auto-appends-Package).
Adding a new iOS source file to the test target
If the code under test references types in a file not yet in Package.swift, add it to the sources: array in the BcscCoreTestable target. If that file depends on types from excluded files (e.g., JWE/crypto), add stubs to SPMCompat.swift behind #if SPM_BUILD.
Adding a new iOS test file
- Create a new
.swiftfile inios/BcscCoreTests/. - Import frameworks and the testable module:
import LocalAuthentication // if testing LAPolicy/biometric types import XCTest @testable import BcscCoreTestable - Write
XCTestCasesubclasses. Follow the naming convention{ClassUnderTest}Tests. - Run with
yarn test:iosor directly:xcodebuild test -scheme BcscCoreTests-Package \ -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ -skipPackagePluginValidation
Android — JUnit 4 + MockK
Tests live under android/src/test/java/com/bcsccore/ mirroring the main source package structure.
Dependencies (in android/build.gradle)
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.13.8'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation 'androidx.test:core:1.5.0'
Adding a new Android test file
- Create a Kotlin file in
android/src/test/java/com/bcsccore/{subpackage}/, matching the source package. - Write standard JUnit 4 test classes with
@Testmethods. - Use
io.mockk.mockk<T>()andevery { ... } returns ...for mocking. - Run with
yarn test:androidor directly:cd android && ./gradlew test
Running all tests
yarn test # runs test:ios then test:android
yarn test:ios # iOS only
yarn test:android # Android only
Mocking Strategy
iOS — Protocol Conformance
The codebase defines protocols for all service boundaries. Create mock classes that conform to the protocol and expose controllable properties:
final class MockPINService: PINServiceProtocol {
var verifyResult: String? // control return value
var setPINResult: String = "mock-hash"
var deletePINCalled = false // track side effects
func verifyPINAndGetHash(issuer _: String, accountID _: String, pin _: String) -> String? {
return verifyResult
}
func setPIN(issuer _: String, accountID _: String, pin _: String, isAutoGenerated _: Bool) throws -> String {
return setPINResult
}
func deletePIN(issuer _: String, accountID _: String) throws {
deletePINCalled = true
}
// ... remaining protocol methods
}
Pattern: Use var resultProperty for controlling return values and var methodCalled = false booleans for verifying side effects.
Available iOS protocols for mocking
| Protocol | File | Purpose |
|---|---|---|
PINServiceProtocol |
PINServiceProtocol.swift |
PIN set/verify/delete/hash |
PINKeychainServiceProtocol |
PINKeychainService.swift |
Keychain secret storage |
PINCryptoPolicyProtocol |
PINCryptoPolicy.swift |
Salt/key/iteration config |
CommonCryptoWrapperProtocol |
CommonCryptoWrapper.swift |
PBKDF2 key derivation |
LAContextProtocol |
LAContext+Extensions.swift |
Biometric evaluation |
DeviceAuthServiceProtocol |
DeviceAuthService.swift |
Device authentication |
TokenStorageServiceProtocol |
TokenService.swift |
Token keychain storage |
KeyPairManagerProtocol |
KeyPairManager.swift |
Cryptographic key pairs |
Android — MockK
Use MockK's mockk<T>() for mocking classes and interfaces:
val mockPinService = mockk<PinService>()
every { mockPinService.validatePIN(any(), "1234") } returns true
every { mockPinService.hasPIN(any()) } returns true
val account = Account(id = "test", issuer = "https://idsit.gov.bc.ca", clientID = "client")
val result = account.verifyPIN("1234", mockPinService)
assertTrue(result.success)
For data classes (e.g., NativeAccount), test serialization round-trips with Gson directly — no mocking needed.
Test Patterns
Time-dependent tests (eg. PIN penalty logic)
The penalty system locks users out after failed PIN attempts. Both platforms accept an injectable "current time" parameter to make tests deterministic.
iOS: Pass verifyDate: parameter:
let start = Date()
for i in 0..<5 {
_ = account.verifyPIN("wrong", pinService: mock, verifyDate: start.addingTimeInterval(Double(i)))
}
// Advance past penalty before next batch
let afterPenalty = start.addingTimeInterval(65)
Android: Pass timestamp to verifyPIN and isServingPenalty:
val now = System.currentTimeMillis()
account.verifyPIN("wrong", mockPinService, now)
val penalty = account.isServingPenalty(now)
Important: When testing escalating penalties, you must advance time past each penalty tier before triggering the next. Calling verifyPIN while a penalty is active will return "locked" without incrementing the attempt counter.
Penalty tiers: 5 attempts → 1 min, 10 → 10 min, 15 → 1 hour, 20 → 1 day.
NSCoding / Serialization round-trips
iOS — Use NSKeyedArchiver / NSKeyedUnarchiver:
let data = try NSKeyedArchiver.archivedData(withRootObject: original, requiringSecureCoding: true)
let decoded = try NSKeyedUnarchiver.unarchivedObject(ofClass: Account.self, from: data)
XCTAssertEqual(decoded?.id, original.id)
Android — Use Gson:
val json = gson.toJson(original)
val restored = gson.fromJson(json, NativeAccount::class.java)
assertEquals(original.uuid, restored.uuid)
Test naming conventions
iOS: test{Behavior} in camelCase — e.g., testVerifyPINReturnsSuccessOnCorrectPIN
Android: Backtick-quoted descriptive names — e.g., `verifyPIN returns success when PIN is correct`
Helper factories
Android — Create a createAccount() helper with defaults to reduce boilerplate:
private fun createAccount(
id: String = "test-uuid",
issuer: String = "https://idsit.gov.bc.ca",
clientID: String = "test-client-id",
securityMethod: AccountSecurityMethod = AccountSecurityMethod.PIN_NO_DEVICE_AUTH,
): Account = Account(id = id, issuer = issuer, clientID = clientID, securityMethod = securityMethod)
Checklist for adding tests
- Identify the class/struct to test and which platform(s)
- Check if the source file is already in
Package.swiftsources:(iOS) — add if missing - Check if new dependencies need stubs in
SPMCompat.swift(iOS) - Create mock(s) for any protocol/interface dependencies
- Place test file in
ios/BcscCoreTests/orandroid/src/test/java/com/bcsccore/{subpackage}/ - For time-dependent logic, use injectable timestamps rather than
Date()/System.currentTimeMillis() - Run
yarn testand verify all tests pass on both platforms
<!-- tomevault:4.0:skill_md:2026-05-23 -->Source: bcgov/bc-wallet-mobile — distributed by TomeVault.
- Fluxly category
- Writing
- Author-declared agents
- No explicit declaration found; this is not inferred or tested compatibility
- Static check
- 88 / 100 · heuristic scan, not runtime safety proof
- Author / version / license
- @tomevault-io · no license declared
- Fluxly token estimate
- Lean
- Fluxly setup estimate
- Plug-and-play
- External API key
- No requirement detected
- Detected OS requirements
- Unspecified
- Runtime requirements
- Unspecified
- Detected file/system behavior
-
- Read-only
- Write / modify
- Detected network behavior
- Local-only
- Install commands
- None (reference only)
Profile is derived at build time from SKILL.md and install vectors. Subject to drift from author intent.
Heads up: 未限定 allowed-tools,默认拥有全部工具权限。
The current SKILL.md does not define a fixed output example. This skill guides creation of native unit tests for the packages/bcsc-core module, which contains Swift (iOS) and Kotlin (Android) implementations of authentication, PIN management, keychain storage, token services, and device authentication.
Test Infrastructure
Tests are built with Swift Package Manager using the package manifest at packages/bcsc-core/Package.swift. The key concepts: BcscCoreTestable target: A subset of production iOS source files compiled for the simulator (without React Native / CocoaPods…
Tests live under android/src/test/java/com/bcsccore/ mirroring the main source package structure. Dependencies (in android/build.gradle) Adding a new Android test file
Running all tests
Mocking Strategy
# bcsc-core Native Unit Testing
## Overview
This skill guides creation of native unit tests for the `packages/bcsc-core` module, which contains Swift (iOS) and Kotlin (Android) implementations of authentication, PIN management, keychain storage, token services, and device authentication.
The codebase already uses **protocol-based dependency injection** on iOS and **interface/class-based injection** on Android, making it straightforward to create mock implementations for isolated unit testing.
## Test Infrastructure
### iOS — XCTest via SPM
Tests are built with Swift Package Manager using the package manifest at `packages/bcsc-core/Package.swift`. The key concepts:
- **`BcscCoreTestable` target**: A subset of production iOS source files compiled for the simulator (without React Native / CocoaPods dependencies). Only files listed in the `sources:` array are included.
- **`BcscCoreTests` test target**: XCTest files under `ios/BcscCoreTests/`, compiled with `@testable import BcscCoreTestable`.
- **`SPM_BUILD` flag**: A Swift compiler define set in `Package.swift` via `swiftSettings: [.define("SPM_BUILD")]`. The file `ios/SPMCompat.swift` uses `#if SPM_BUILD` to provide lightweight stubs (e.g., `SecureRandom`, `Data.arrayOfBytes()`) for types from JWE files that aren't included in the SPM target.
- **Scheme name**: xcodebuild uses `BcscCoreTests-Package` (SPM auto-appends `-Package`).
#### Adding a new iOS source file to the test target
If the code under test references types in a file not yet in `Package.swift`, add it to the `sources:` array in the `BcscCoreTestable` target. If that file depends on types from excluded files (e.g., JWE/crypto), add stubs to `SPMCompat.swift` behind `#if SPM_BUILD`.
#### Adding a new iOS test file
… Author text anchors workflow facts; Fluxly only indexes current sections, terms, files, and commands.
sections -> Overview → Test Infrastructure → iOS — XCTest via SPM → Android — JUnit 4 + MockK → Running all tests → Mocking Strategy
terms -> protocol-based dependency injection · interface/class-based injection · BcscCoreTestable target · BcscCoreTests test target · SPMBUILD flag · Scheme name · Pattern · iOS
files/cmd -> packages/bcsc-core · packages/bcsc-core/Package.swift · BcscCoreTestable · sources: · BcscCoreTests · ios/BcscCoreTests/ · @testable import BcscCoreTestable · SPMBUILD
body sha256 -> 9ce69e88c59e
Decide Fit First
Design Intent
How To Use It
Boundaries And Review