Injecting BitmapFrameSource for Android Scandit SDK UI Tests

Summary

The engineering team encountered a blocker during the implementation of automated UI tests for an Android application utilizing the Scandit Data Capture SDK. While the production environment successfully utilizes the hardware camera, the testing suite requires a way to simulate barcode scanning by injecting static images. The core challenge lies in whether the BitmapFrameSource API can be reliably leveraged within an instrumentation test environment—specifically on emulators—without modifying the underlying production source code.

Root Cause

The inability to perform seamless UI testing in this scenario stems from several architectural and environmental factors:

  • Hardware Abstraction Layers: The SDK is designed to interact with the physical camera subsystem. In an automated test environment, the “camera” is often a virtualized or null device, making traditional scanning impossible.
  • State Management Constraints: The current production implementation uses a conditional branch that switches the FrameSource based on the presence of a Bitmap. While BitmapFrameSource is the correct programmatic way to inject frames, the test runner must find a way to inject this dependency without a direct refactor of the production logic.
  • Emulator Limitations: Android emulators lack the sophisticated GPU/ISP pipeline required to mimic real-time camera streams, often leading to timing issues or memory pressure when processing high-resolution bitmaps through the SDK’s processing engine.

Why This Happens in Real Systems

This issue is a classic example of Tight Coupling between Business Logic and Hardware Providers.

  • Dependency Inversion Failure: The application logic directly decides which FrameSource to use based on a null check, rather than receiving a FrameSource via Dependency Injection (DI).
  • Testability vs. Operability: Systems are often optimized for the “Happy Path” (the user with a camera) but fail to account for the “Test Path” (the automation engine with a file).
  • Environmental Divergence: Production environments use real hardware with deterministic drivers, while CI/CD environments use emulators with non-deterministic, emulated hardware.

Real-World Impact

Failure to solve this testing bottleneck results in:

  • Reduced Test Coverage: Barcode scanning logic remains untested in CI, leading to “blind spots” where regressions in decoding logic can slip into production.
  • Flaky Test Suites: Attempts to use the emulated camera often lead to intermittent failures, causing developers to ignore test results (the “broken window” effect).
  • Increased Release Risk: Without automated image-based scanning tests, the team must rely on manual QA to verify every new barcode type or SDK update.

Example or Code

To solve this without modifying production code, one must use Dependency Injection or Bytecode Manipulation to force the bitmap variable to be non-null during the test execution.

// Hypothetical Test Implementation using a Mocking framework or DI
@Test
fun testBarcodeScanningWithBitmap() {
    val testBitmap = BitmapFactory.decodeResource(
        InstrumentationRegistry.getInstrumentation().context.resources,
        R.drawable.sample_barcode
    )

    // Assuming the ScanningService is injectable via Dagger/Hilt
    // We inject a version of the service that accepts a BitmapFrameSource
    val scannerComponent = scannerComponentModule.getTestComponent()

    // Trigger the logic that would normally receive the bitmap
    scannerComponent.injectBitmap(testBitmap)

    // Assert that the scanner correctly identifies the barcode in the image
    onView(withId(R.id.scanned_result_text))
        .check(matches(withText("123456789")))
}

How Senior Engineers Fix It

A senior engineer looks past the immediate “How do I make this work?” and asks “How do I make this testable by design?”.

  • Refactor for Dependency Injection: Instead of checking if (bitmap != null) inside the Activity/Fragment, the FrameSource should be provided by a FrameSourceFactory. In production, the factory returns a CameraFrameSource; in tests, it returns a BitmapFrameSource.
  • Implement a Test-Only Flavor: Use Android Build Variants to provide a “Mock Scanner” implementation that allows for easier image injection during instrumentation.
  • Abstraction of Input: Move the scanning logic into a ViewModel or a Use Case layer that operates on an abstraction of the camera, making it decoupled from the Android Lifecycle and hardware.

Why Juniors Miss It

  • Focus on the “How” instead of the “Why”: Juniors often try to find a “magic flag” in the SDK to enable testing, rather than realizing the architectural design is what prevents the test.
  • Workaround Mentality: A junior might attempt to use adb shell to push images to the emulator camera, which is brittle and extremely difficult to synchronize with the SDK’s internal processing loop.
  • Ignoring the Lifecycle: They may not realize that BitmapFrameSource.switchToDesiredState(FrameSourceState.ON) involves asynchronous state transitions that require proper Idling Resources in UI tests to prevent race conditions.

Leave a Comment