9.1 KiB
Building, running, and debugging instrumentation tests
Both APKs matter
Instrumentation tests need TWO APKs:
:app:assembleGoogleMocked→app-google-mocked.apk— production code under test:app:assembleGoogleMockedAndroidTest→app-google-mocked-androidTest.apk— the test code
If you change production code and rebuild only the test APK, the installed main APK stays old and your fix doesn't take effect. Symptom: "the fix doesn't help" — except it does, you just ran the unfixed build.
# Build both
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
# Install each
adb install -r -t <path-to-app-google-mocked.apk>
adb install -r -t <path-to-app-google-mocked-androidTest.apk>
Run a single test (manual)
adb shell pm clear com.tangem.wallet.mocked
curl -X POST http://localhost:8081/__admin/scenarios/reset
adb shell am instrument -w \
-e class "com.tangem.tests.tangempay.TangemPayTest#freezeUnfreezeCard_TogglesCardState" \
com.tangem.wallet.mocked.test/com.tangem.common.HiltTestRunner
Harness: orchestrator vs. raw am instrument
The app is configured execution = "ANDROIDX_TEST_ORCHESTRATOR" (app/build.gradle.kts). The orchestrator
runs each test method in its own process (and can clear app data between them). It is still 100%
local — it runs on the same emulator; nothing remote about it.
Raw adb shell am instrument runs all selected tests in one shared process, which has two failure
modes that look like test bugs but aren't:
- Running several tests in one invocation →
IllegalStateException: There are multiple DataStores active for the same filemid-run. Run them one at a time (withpm clearbetween) if you must use rawam instrument. - Tests that re-scan the card inside Card/Device Settings (the "Scan card or ring" gate) →
IllegalStateException: Tangem SDK is null after re-registering with foreground activity. The existingResetCardTestcrashes identically under rawam instrument. These only pass via the orchestrator.
Prefer the orchestrator (it's what CI/Marathon use). Run a class or method through Gradle:
./gradlew :app:connectedGoogleMockedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest
# or a single method: ...class=com.tangem.tests.DetailsTest#someTest
# or several classes: ...class=com.tangem.tests.DetailsTest,com.tangem.tests.SecurityModeTest
Gradle installs both APKs, runs via the orchestrator, then uninstalls them — so a following raw
am instrument reports Unable to find instrumentation info; reinstall both APKs first. Read results
from the JUnit XML (authoritative pass/fail counts), not just stdout:
ls -t app/build/outputs/androidTest-results/connected/mocked/flavors/google/*.xml | head -1
# inspect tests="…" failures="…" errors="…" skipped="…" and the <testcase>/<failure> nodes
Running against local WireMock
Every instrumentation test runs with ApiEnvironment.MOCK (forced in BaseTestCase.setupHooks), so the
app's API base URLs point at wiremock.tests-d.com — i.e. tests always talk to WireMock, never the
real backend. By default that's the remote WireMock at wiremock.tests-d.com. To use a local
WireMock instead, pass wiremockBaseUrl: WireMockRedirectInterceptor then rewrites every
wiremock.tests-d.com request to your local instance.
Emulator addressing matters — localhost inside an emulator is the emulator itself, not your host:
- Use the host alias
http://10.0.2.2:8081(no extra setup), or http://localhost:8081withadb reverse tcp:8081 tcp:8081run first.
Pass it through the orchestrator (recommended):
curl -s -X POST http://localhost:8081/__admin/scenarios/reset # start clean
./gradlew :app:connectedGoogleMockedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest \
-Pandroid.testInstrumentationRunnerArguments.wiremockBaseUrl=http://10.0.2.2:8081
(Raw am instrument equivalent: -e wiremockBaseUrl http://10.0.2.2:8081 — subject to the harness
caveats above.)
If a screen hangs / you get ComposeNotIdleException (infinite recomposition): that usually means a
request the app made wasn't served (endless retry/loading), not a test bug. Ask WireMock what it
didn't match — this is the smoking gun:
curl -s http://localhost:8081/__admin/requests/unmatched | jq '.requests[] | "\(.method) \(.url)"'
unmatched: 0 means the URL plumbing is correct and local WireMock served everything — look elsewhere
(harness/emulator) for the hang. A non-empty list names exactly which mapping (or scenario state) the
local instance is missing.
When the UI fails silently, the cause is usually app-side — two places to look
A screen failing silently with correct locators (fee shows "—", a banner never appears, a button stays disabled) is usually missing mock data or an app-side gate, not a test bug. Two diagnostics find it:
- The app's own log is in
files/log.txt, not logcat — the mocked build routesTangemLoggerto a file, soadb logcatshows nothing. Fastest path to a root cause (e.g. it surfacedIllegalStateException: No native currency found→ a native coin missing from the mock):adb exec-out run-as <pkg> cat files/log.txt | grep -iE "Error|Exception|<feature>" - The WireMock journal separates "mock missing" from "app never asked" —
/__admin/requests/unmatchedfinds missing mappings, but ifunmatched=0and the expected request is also absent from the full log (/__admin/requests), the app never issued it (a data/state gate) → fix the mock data or the app, not the mappings.
"UiAutomationService already registered" — retry, it's not a failure
Back-to-back am instrument runs sometimes fail instantly with UiAutomationService … already registered! — a teardown race between runs, not a test failure. Retry. (The orchestrator avoids it by
spacing runs — another reason to confirm a flaky-looking suite via the orchestrator, not raw
am instrument.)
Classify the result — Allure noise vs. real failure
After pm clear, /data/user/0/<pkg>/files/original_screenshots doesn't exist →
AllureResultsHack.testRunFinished throws NoSuchFileException → reported as
Tests run: 1, Failures: 1 with a stack trace starting at AllureResultsHack. This is a post-run
hook failure, NOT a test logic failure.
Distinguish:
- First stack frame is
AllureResultsHack.testRunFinished→ infra hook noise; ignore it. - Kaspresso step logs show all
SUCCEEDfor steps 1..N → the test passed. - A REAL failure shows
java.lang.AssertionErrorinside the test's own classes (e.g.at com.tangem.tests.X.foo$lambda…). When auto-classifying CLI output, key off the presence ofjava.lang.AssertionErrorvs. onlyoriginal_screenshots.
@Ignore on instrumentation tests
- Pattern:
@Ignore("https://tangem.atlassian.net/browse/AND-XXXXX")above@Test. - When ignored,
am instrument -e class …reportsOK (0 tests)withTests run: 0(NOTSkipped: 1). Auto-detection should match the zero-test count.
WireMock cheatsheet
Without a wiremockBaseUrl arg the app hits the remote WireMock (wiremock.tests-d.com); pass the
arg to redirect to a local instance (see "Running against local WireMock"). Default local port: 8081.
# Set a scenario state — PUT, not POST
curl -X PUT http://localhost:8081/__admin/scenarios/<name>/state \
-H "Content-Type: application/json" -d '{"state":"<state>"}'
# Reset all scenarios
curl -X POST http://localhost:8081/__admin/scenarios/reset
# Inspect
curl http://localhost:8081/__admin/mappings | jq
curl http://localhost:8081/__admin/scenarios | jq '.scenarios[] | {name, state}'
- Mocks repo: default to the sibling directory
../tangem-api-mocks/(i.e. next totangem-app-android). If that path doesn't exist, ask the user where the mocks repo is rather than guessing. - The repo is branch-per-suite — dozens of feature branches (e.g.
send-via-swap-p1,account-creation,swap-express-mocks,android-tangem-pay-mocks). There is no universal default branch; check out the one the suite under test expects. If it's unclear which branch holds the mappings for your flow, ask the user. Mappings live undermocks/mappings/, response bodies undermocks/__files/. - State transitions are atomic per
requiredScenarioState. If a scenario defines anAfterDepositmapping for/customer/balancebut not/customer/me, a request to/customer/meafter switching toAfterDepositfalls through. Check both endpoints when an "after" assertion fails.
Misc
./gradlew unitTestaggregates all debug/googleDebug + JVM-module tests — faster than per-module tasks for verifying a broad change (but it's for unit tests, not instrumentation).- Detekt config lives in the
tangem-android-toolsgit submodule — look there before assuming a local.detekt.yml. - Path discipline: stay at the repo root;
cdinto the mocks repo only when needed and prefer absolute paths (the shell session resets cwd).