tangem-app-android-audited/.claude/skills/write-ui-test/reference/running-and-debugging.md
2026-06-24 17:38:32 +02:00

200 lines
No EOL
9.9 KiB
Markdown

# 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.
```bash
# 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)
```bash
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 file` mid-run. Run them one at a time (with `pm clear` between) if you must use raw
`am 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 existing
`ResetCardTest` crashes identically under raw `am instrument`. These only pass via the orchestrator.
**Prefer the orchestrator** (it's what CI/Marathon use). Run a class or method through Gradle:
```bash
./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:
```bash
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:8081` **with** `adb reverse tcp:8081 tcp:8081` run first.
Pass it through the orchestrator (recommended):
```bash
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:
```bash
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.
## The app's own log lives in `files/log.txt`, NOT logcat
The mocked build routes `TangemLogger` to a **file** via `FileLogWriter`, so `adb logcat | grep …`
finds nothing of the app's own logs. When a screen fails silently — fee shows "—", a banner never
appears, an action button stays disabled — *and* WireMock shows everything matched, the real reason is
almost always in the app log:
```bash
adb exec-out run-as com.tangem.wallet.mocked cat files/log.txt | grep -iE "loadFee|getFee|No native|Error|DataError"
```
This is the single fastest way to find app-side root causes. It's what pinpointed a transfer-fee failure
to `loadFee[transfer]: DataError(... IllegalStateException: No native currency found ...)` — i.e. a
missing native coin in the mock, invisible from the UI and from the WireMock journal alone.
## WireMock journal: "mock missing" vs "the app never asked"
`/__admin/requests/unmatched` finds *missing* mappings. But when a feature silently doesn't happen (a fee
that never computes, a banner that never shows), also inspect the **full** request log — the app may not
be issuing the request at all (an app-side data gate), which is a different problem than a missing mock
and is NOT fixable by adding mappings:
```bash
curl -s "http://localhost:8081/__admin/requests?limit=500" \
| jq -r '.requests[].request | "\(.method) \(.url)"' | sort -u
# For RPC providers, also break down by method:
curl -s "http://localhost:8081/__admin/requests?limit=500" \
| jq -r '.requests[].request.body' | grep -oE '"method":"[^"]+"' | sort | uniq -c
```
`unmatched=0` **and** the expected request absent → the app never asked (data/state gate, e.g. a coin
missing from the portfolio) → fix the mock *data* or app, not the mappings.
## "UiAutomationService already registered" = back-to-back runs; retry
Rapid consecutive `am instrument` invocations sometimes fail instantly with
`IllegalStateException: UiAutomationService … already registered!`. It's an instrumentation-teardown
race between runs, not a test failure — just retry. (The orchestrator/CI spaces runs out and avoids it.)
When scripting many manual runs, retry-on-this-string rather than counting it as a failure.
## 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 `SUCCEED` for steps 1..N → the test passed.
- A REAL failure shows `java.lang.AssertionError` inside the test's own classes
(e.g. `at com.tangem.tests.X.foo$lambda…`). When auto-classifying CLI output, key off the presence
of `java.lang.AssertionError` vs. only `original_screenshots`.
## `@Ignore` on instrumentation tests
- Pattern: `@Ignore("https://tangem.atlassian.net/browse/AND-XXXXX")` above `@Test`.
- When ignored, `am instrument -e class …` reports `OK (0 tests)` with `Tests run: 0`
(NOT `Skipped: 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`.
```bash
# 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 to
`tangem-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 under `mocks/mappings/`, response bodies
under `mocks/__files/`.
- **State transitions are atomic per `requiredScenarioState`.** If a scenario defines an `AfterDeposit`
mapping for `/customer/balance` but not `/customer/me`, a request to `/customer/me` after switching
to `AfterDeposit` falls through. Check *both* endpoints when an "after" assertion fails.
## Misc
- `./gradlew unitTest` aggregates 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-tools` git submodule — look there before assuming a local
`.detekt.yml`.
- Path discipline: stay at the repo root; `cd` into the mocks repo only when needed and prefer absolute
paths (the shell session resets cwd).