-
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 two cross-SDK parity suites, because there are two bucketing layouts. Both drive golden vectors captured from the JavaScript SDK, and both must pass 100% — they are the gate that proves a visitor lands in the same variation on every Convert SDK.
74 golden vectors 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 HashParityTestsUses swift-testing (@Suite("HashParity")). 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).
59 golden vectors driven end-to-end through BucketingManager.bucketVersionGated, the version gate that routes an experience to the right layout: an experience whose version is greater than 11 selects under the anchored layout, anything at or below 11 delegates verbatim to the existing packed walk. The fixture carries full experience shapes (version, per-variation traffic_allocation and status), so the suite asserts the resolved variation id — not just a bucket value:
# from ios-sdk/
swift test --filter AnchoredBucketingParityTestsThe companion AnchoredBucketingGateAndBoundaryTests covers what a shared fixture cannot: the version-gate sweep, anchor/width boundary conditions, bit-for-bit v11 delegation, sticky decisions winning over both layouts, and the unchanged bucketing-event shape.
Run all three before shipping any change to MurmurHash3, BucketingManager, or AnchoredBucketing:
swift test --filter HashParityTests
swift test --filter AnchoredBucketingParityTests
swift test --filter AnchoredBucketingGateAndBoundaryTests-
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 # hash + packed-layout parity gate
swift test --filter AnchoredBucketingParityTests # anchored-layout parity gate
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
- Direct Tracking Endpoint
- QA & Preview
- Mutually Exclusive Experiments
Contributing