Testing the tk_ota_push SDK
This guide details how to run the SDK's test suite, build custom test doubles (FakeShorebirdUpdater, FakeDeviceIntegrityInspector, MockClient), and validate the entire OTA update lifecycle on Android emulators and physical iOS hardware.
1. Running the Included Unit Test Suite
The tk_ota_push package includes 6 test suites covering the mobile runtime and the deployment CLI in tk_ota_push/test/:
| Test File | Scope Verified |
|---|---|
test/tk_ota_push_test.dart | 3-step Gatekeeper, silent updates, workmanager deferrals under 3G/4G, critical forced updates, rollback detection (OtaRollbackTracker), APK anti-repackaging (Xender vs pirated APK), root/jailbreak detection, TLS pinning, OtaAssetManager, and readClientId / readShortSupportCode / readDeviceIdentity. |
test/native_app_id_detection_test.dart | 9-level app_id resolution cascade, Gradle Kotlin DSL / Groovy detection (productFlavors, buildTypes, applicationIdSuffix), and Xcode project.pbxproj. |
test/flavor_build_args_test.dart | Flutter argument assembly (--flavor, -t, --dart-define, --dart-define-from-file, --artifact, --export-method). |
test/native_integrity_test.dart | NativeIntegrityScanner on Android archives (classes*.dex, AndroidManifest.xml, lib/*.so excluding application binary) and iOS bundles (Info.plist, Frameworks/). |
test/geo_rollout_cli_test.dart | ISO-2 country code parsing (--countries, geo.default_countries). |
test/cli_auth_test.dart | CLI authentication workflows and local session credential management. |
To execute all tests:
cd tk_ota_push
dart test
2. Mocking the Rust Engine & Environment in Unit Tests
During standard unit testing (flutter test / dart test), the native C++/Rust engine is not loaded (ShorebirdUpdater().isAvailable == false). All components of TkOtaPush are fully injectable through constructors to allow deterministic testing of your startup logic.
Building FakeShorebirdUpdater and FakeDeviceIntegrityInspector
import 'package:shorebird_code_push/shorebird_code_push.dart';
import 'package:tk_ota_push/tk_ota_push.dart';
class FakeShorebirdUpdater implements ShorebirdUpdater {
FakeShorebirdUpdater({
this.available = true,
this.installedPatchNumber,
this.nextStatus = UpdateStatus.outdated,
});
final bool available;
int? installedPatchNumber;
UpdateStatus nextStatus;
int checkForUpdateCalls = 0;
int updateCalls = 0;
bool get isAvailable => available;
Future<Patch?> readCurrentPatch() async =>
installedPatchNumber == null ? null : Patch(number: installedPatchNumber!);
Future<Patch?> readNextPatch() async => readCurrentPatch();
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async {
checkForUpdateCalls++;
return nextStatus;
}
Future<void> update({UpdateTrack? track}) async {
updateCalls++;
}
}
class FakeDeviceIntegrityInspector extends OtaDeviceIntegrityInspector {
FakeDeviceIntegrityInspector({
this.installedApkSignatureSha256,
this.rootedOrJailbroken = false,
});
String? installedApkSignatureSha256;
bool rootedOrJailbroken;
Future<String?> readInstalledApkSignatureSha256() async =>
installedApkSignatureSha256;
Future<bool> isDeviceRootedOrJailbroken() async => rootedOrJailbroken;
}
Mocking Server Responses for OtaAssetManager with MockClient
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:tk_ota_push/tk_ota_push.dart';
Future<void> testAssetDownload() async {
final payload = utf8.encode('{"banner": "Special Offer"}');
final expectedHash = sha256.convert(payload).toString();
final mockHttp = MockClient((request) async {
return http.Response.bytes(payload, 200);
});
final manager = OtaAssetManager(
cacheDirectory: Directory.systemTemp.createTempSync('asset_test_'),
httpClient: mockHttp,
);
final file = await manager.getOrDownloadAsset(
OtaRemoteAssetDescriptor(
assetKey: 'assets/config/banner.json',
downloadUrl: 'https://ota.internal/assets/banner.json',
expectedSha256: expectedHash,
),
);
assert(file.existsSync());
}
3. Testing on Android Emulators or Physical Smartphones
The native OTA engine only activates in Release Mode built via tk-ota-push release. An application launched with flutter run (debug JIT mode) always reports isAvailable == false.
Network Configuration (base_url)
- On a physical Android smartphone connected to the same Wi-Fi as your development machine:
Set your computer's LAN IP in
shorebird.yaml(e.g.,base_url: "http://192.168.0.199:8080") and verifyandroid:usesCleartextTraffic="true"is enabled inAndroidManifest.xml(development only). - On Android Studio Emulator (AVD):
Use
base_url: "http://10.0.2.2:8080"(the host machine's loopback alias) or reverse the port via ADB:adb reverse tcp:8080 tcp:8080
End-to-End Android Testing Protocol
# 1. Compile and seal baseline Release 1.0.0+1
tk-ota-push release -p android
# 2. Install sealed APK onto phone or emulator
adb install -r build/app/outputs/flutter-apk/app-release.apk
# 3. Launch the application once (generates client_id in state.json)
# 4. Modify a UI element in lib/main.dart, then deploy Patch #1
tk-ota-push patch -p android -m "Test Patch #1"
# 5. Trigger sync in the app (or restart if auto_update: true)
# 6. Cold-kill and restart the application: Patch #1 is rendered!
To tail the native Rust engine logs live on Android:
adb logcat | grep -i shorebird
4. Testing on Physical iOS Devices
The iOS OTA engine (gen_snapshot_arm64 / patch artifact) targets physical aarch64 hardware:
# 1. Compile iOS Release (exporting Ad-Hoc or Development IPA)
tk-ota-push release -p ios --artifact ipa --export-method development
# 2. Install compiled application onto the physical iPhone
flutter install -d <IPHONE_DEVICE_ID>
# 3. Modify lib/main.dart and deploy Patch #1
tk-ota-push patch -p ios -m "Test Patch iOS #1"