-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
This page covers testing your app's integration with the Convert iOS SDK — making experiment decisions deterministic in tests, keeping tracking quiet, and using the SDK's injection seams. For the SDK's own internal test suite, see the repository's Tests/ directory.
The SDK's own suites use swift-testing (import Testing, @Suite, @Test, #expect) throughout. Your integration tests can use either swift-testing or XCTest — the SDK's public surface is framework-agnostic.
The biggest source of test flakiness is the asynchronous config fetch. Avoid it entirely by constructing the SDK with a pre-loaded config payload (init(configData:)) — decisions are available as soon as ready() resolves and never depend on the network:
import ConvertSwiftSDK
import Testing
@Test func bucketsVisitorDeterministically() async throws {
let configData = try Data(contentsOf: Bundle.module.url(
forResource: "test-config", withExtension: "json")!)
let sdk = ConvertSwiftSDK(configData: configData)
try await sdk.ready()
let context = sdk.createContext(visitorId: "test-visitor")
let variation = await context.runExperience("homepage-redesign")
#expect(variation?.key == "control") // stable because visitorId is fixed
}With init(configData:), the SDK validates the payload directly without a network call. Use a stable explicit visitor id (e.g. "test-visitor") so bucketing is reproducible run to run — the same (experienceId, visitorId) pair always maps to the same variation (MurmurHash3 seed 9999, deterministic).
For XCTest:
import ConvertSwiftSDK
import XCTest
class ConvertIntegrationTests: XCTestCase {
func testBucketsVisitorDeterministically() async throws {
let configData = try Data(contentsOf: Bundle.module.url(
forResource: "test-config", withExtension: "json")!)
let sdk = ConvertSwiftSDK(configData: configData)
try await sdk.ready()
let context = sdk.createContext(visitorId: "test-visitor")
let variation = await context.runExperience("homepage-redesign")
XCTAssertEqual(variation?.key, "control")
}
}Disable outbound tracking so tests do not emit network events:
let config = ConvertConfiguration(sdkKey: "test-key", networkTracking: false)
let sdk = ConvertSwiftSDK(configuration: config)Bucketing, rule evaluation, and sticky persistence still work with tracking disabled — only the network side is silenced. See Tracking Control. For a single call, use the per-call override: await context.runExperience("key", enableTracking: false).
With init(configData:) the SDK never makes a config network call; pair it with networkTracking: false for full network isolation:
let sdk = ConvertSwiftSDK(configData: configData)
// For a sdk constructed with configData, tracking suppression at the EventQueue level
// still applies — either pass ConvertConfiguration(sdkKey:, networkTracking: false)
// via init(configuration:directData:) or use the runtime toggle:
await sdk.setTrackingEnabled(false)Unlike the Android SDK (which requires Robolectric to run on the JVM), the iOS SDK's core (ConvertSwiftSDKCore) is pure Swift with no UIKit/AppKit dependency. Tests against ConvertSwiftSDKCore types run as plain JVM-equivalent Swift unit tests — you do not need a simulator or device for core logic. Platform-layer tests (ConvertSwiftSDKTests) that exercise BackgroundSessionManager, LifecycleObserver, or Keychain adapters do require a simulator or macOS.
The SDK logs through the Logger port. Inject a custom logger to assert on log output:
import ConvertSwiftSDK
// Use the @testable import to reach the internal initializer
@testable import ConvertSwiftSDK
// A simple recording logger (not provided by the SDK — implement your own or use MockLogger
// from the SDK's own test support if your test target is @testable-importing ConvertSwiftSDK):
actor RecordingLogger: Logger {
private(set) var lines: [String] = []
func log(level: LogLevel, type: String, method: String, message: String) {
lines.append("[\(level)] \(type).\(method): \(message)")
}
}Note:
MockLoggerinTests/ConvertSwiftSDKTests/Support/MockPorts.swiftis internal to the SDK's own test target. For your integration tests, implement your own recording logger conforming to theLoggerport (public protocol Logger: SendableinConvertSwiftSDKCore), or avoid log assertions and instead assert on the observable effects (returned variations, event counts).
The SDK ships a cross-SDK parity suite at Tests/ConvertSwiftSDKCoreTests/Bucketing/HashParityTests.swift. It drives 74 golden vectors captured from the JavaScript SDK, proving that MurmurHash3 (x86, 32-bit, seed 9999) and BucketingManager.selectBucket produce byte-for-byte identical decisions to the JS SDK:
# from ios-sdk/
swift test --filter HashParityTestsThe parity suite uses swift-testing (@Suite("HashParity"), @Test("cross-SDK parity vector", arguments: vectors)). Every hash key is "<experienceId><visitorId>" (experience id first, no separator), seeded with the vector's own seed (most are 9999; a few probe edge-case seeds). All 74 vectors must pass 100%; this gate must stay green before any change to bucketing, hash, or rule-matching logic.
Run it before shipping any change to MurmurHash3.swift or BucketingManager.swift.
-
Fresh SDK per test — construct a new
ConvertSwiftSDKper test (or per class) so visitor and queue state do not leak. The defaultDecisionStoreuses an on-diskApplicationSupportFileStoreat a process-shared path; for unit isolation, use the@testable importinternal init with an in-memoryMockFileStore-backedDecisionStore(as the SDK's own test suites do). - Use explicit visitor ids — never rely on the auto-UUID in tests; an explicit id makes bucketing assertions stable across runs.
-
Prefer fixtures over live keys —
init(configData:)with a checked-in config fixture keeps tests hermetic and fast. Real config captures live inTests/ConvertSwiftSDKCoreTests/Fixtures/. -
Avoid wall-clock assertions — the SDK's own suites inject a
MockClockvia the internalclock:init parameter to make timer-driven behavior deterministic. If your test needs to assert timing (e.g. refresh intervals), use the internal init seam.
# from ios-sdk/
swift test # both targets: ConvertSwiftSDKCoreTests + ConvertSwiftSDKTests
swift test --filter HashParityTests # parity gate only
swift test --filter ConvertSwiftSDKCoreTests # core only (no simulator required)
swift test --filter ConvertSwiftSDKTests # platform layer (simulator / macOS)-
Initialization — direct-data mode and
ready() - Tracking Control — disabling tracking in tests
-
Troubleshooting — diagnosing
nildecisions
Copyrights © 2026 All Rights Reserved by Convert Insights, Inc.
Getting Started
iOS SDK
- Quickstart
- Installation
- Initialization
- Configuration
- Return Types & Models
- Code Examples
- Offline Behavior
- Tracking Control
- App Privacy & Data Collection
- Objective-C Interop
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent Storage
- Troubleshooting
Contributing