team18‐iter2‐testing - snuhcs-course/swpp-2026-project-team-18 GitHub Wiki
This page follows the course's Testing Documentation Guidelines: every testing activity has a Plan (what is tested, how, by whom, to what standard) and Results (what was measured against that plan). The Iteration 1 version used the earlier template and is kept as it was: Testing Documentation, Iteration 1.
| Version | Date | Changes |
|---|---|---|
| 2.0 | 2026-10-18 | Iteration 2: Test Responsibility & Schedule; Unit & Integration Testing plan (frameworks, coverage targets by layer, exclusions, commands) and first results on main 11a8081
|
Status: ✅ met ·
| Component (layer) | Responsible | Tests they write and maintain |
|---|---|---|
| Android app: View, ViewModel, Model (domain, data), Platform | Jinho Kim (frontend, front-iter2) |
JVM unit tests in APP/app/src/test; device integration tests in APP/app/src/androidTest/.../integration
|
Backend: Computation (planning, prediction, routing) and API apps |
Junha Park (backend, back-iter2) |
pytest unit and API integration tests in backend/apps/*/tests; the HTTP contract suites in backend/scripts/check_*.py
|
| End-to-end flows | Junha Park |
PublicSetupTest, DemoFlowTest and the qa/demo/run.py runner |
| CI, coverage tooling, test infrastructure, this page | Sooyeon Kim (PM) |
.github/workflows/ci.yml; coverage tasks and summary scripts; the integration-marker guard (backend/conftest.py); shared fakes (APP/app/src/sharedTest) |
Sooyeon Kim also wrote the Iteration 2 repository tests and device integration tests to close coverage gaps.
Policy for new code
- The author of a feature writes its tests and commits them with the feature, on the same branch,
before the branch is merged into
main. A new endpoint gets an API integration test. A new repository, rule or screen state gets a JVM unit test, and a device integration test when it depends on Android (Room, Compose). - A bug fix comes with a regression test that fails without the fix.
- Tests are never skipped, deleted or loosened to make CI pass. When a behavior change makes a test wrong, the same commit updates the test and says why.
- Backend tests that send a request through the Django test client carry
@pytest.mark.integration.backend/conftest.pyfails any test whose marker doesn't match what it actually did, so the unit and integration counts below can't drift. - No test reaches the shared server, the shared database or a paid external API. CI has no secrets.
| When | What runs | Must pass |
|---|---|---|
Every push to main and every pull request (GitHub Actions, ci.yml) |
Four jobs in parallel: backend pytest with coverage; app–server contract (8 HTTP suites against a live local server); app unit tests with coverage, lint and debug build; app integration tests on an API 34 emulator | All four jobs. Lint blocks errors only |
Before a feature branch is merged into main
|
The same four suites on the developer's machine (commands in 3.5) | All four |
| Before each iteration demo, and after changes to alarms or trip tracking | End-to-end flows through qa/demo/run.py on an emulator |
Every step |
| End of each iteration | Coverage re-measured from the CI reports; Results on this page updated | — |
flowchart LR
dev["Developer"] -->|"push to main or pull request"| ci["GitHub Actions"]
ci --> be["Backend pytest + coverage<br/>702 unit · 522 integration"]
ci --> http["App–server contract<br/>8 HTTP suites, live local server"]
ci --> app["App unit tests + coverage<br/>lint · debug build"]
ci --> emu["App integration tests<br/>API 34 emulator"]
dev -->|"before demos"| e2e["qa/demo/run.py<br/>end-to-end on an emulator"]
In Iteration 2 the team works on front-iter2 and back-iter2, and the PM merges them into
main; pull-request reviews were not used. main has no branch protection yet, so the merge rule
is kept by the team rather than enforced by GitHub: a push that turns main red is fixed before
any other work. When the two branches were merged on 2026-10-17 (26fd3a2), the repository tests
no longer compiled against the new API; 73859f8 fixed them the same day. Making the four jobs
required status checks needs a repository admin.
The end-to-end flows are not in CI: they need the QA build and the route replay server, and take 5–7 minutes on top of the emulator start.
| Level | What one test covers | Where | Runs |
|---|---|---|---|
| Unit — backend | One function, class or management command. The database holds fixtures only |
backend/apps/*/tests, tests without the integration marker |
CI |
| Unit — app (JVM) | One class: domain rules, a repository with a fake API, a screen-state holder | APP/app/src/test |
CI |
| Integration — backend API | One request through the Django test client: URL → middleware → view → serializer → permission → database |
backend/apps/*/tests, tests marked @pytest.mark.integration
|
CI |
| Integration — app–server contract | HTTP against a running Django server. The app's DTO field names are compared with real responses |
backend/scripts/check_*.py, run by run_local_suite.py
|
CI |
| Integration — app on a device | Screens, ViewModels, repositories, Room and navigation together on Android, with fake APIs and no server | APP/app/src/androidTest/.../integration |
CI (emulator) |
| End-to-end | Sign-up → alarm → departure and arrival → upload → weekly report on the real UI |
PublicSetupTest, DemoFlowTest with qa/demo/run.py
|
Before demos |
| Component | Test framework | Mocking and isolation | Coverage |
|---|---|---|---|
| Android app, JVM | JUnit 4.13.2, kotlinx-coroutines-test | Hand-written fakes of the Retrofit API interfaces and SharedPreferences (APP/app/src/sharedTest, shared with the device tests); no mocking library. Rules that would need a device are kept in pure classes (*Decision, TripGeofence) and tested on the JVM |
JaCoCo 0.8.14 through the jacocoDebugUnitTestReport task; APP/scripts/coverage_summary.py groups it by layer |
| Android app, device | AndroidX Test, Compose UI Test (ui-test-junit4), Room; UI Automator for end-to-end |
The same fakes; Room in memory or the app's own database | Not measured yet (see 4.4) |
| Backend | pytest 8.3.4, pytest-django 4.9.0, DRF APIClient
|
config/settings/test.py forces an in-memory SQLite database and blanks every external key. The Kakao, Seoul and LLM clients are replaced at the HTTP layer with monkeypatch fakes |
pytest-cov 6.0.0 (coverage.py 7.16); backend/scripts/coverage_summary.py groups it by component |
| App–server contract | Python scripts over HTTP, run_local_suite.py
|
A live runserver on a throwaway SQLite file; no Kakao key, so route checks are skipped |
— |
The app's UI is Jetpack Compose, so screen tests use Compose UI Test (onNodeWithText,
performClick) instead of Espresso's onView. It uses Espresso's synchronization underneath.
The app follows MVVM: the Model is domain (pure rules) plus data (repositories, network,
local cache), ViewModels live in ui/**, and the View is the Compose UI. A fifth layer holds the
Android services, receivers and workers. The backend is a Django REST server whose apps split
into computation and API. All targets are line coverage.
| Component | Contents | Target | Why |
|---|---|---|---|
| App — Model: Domain |
domain/**: alarm arithmetic, arrival verdicts, route progress |
80% | The core calculations. Pure Kotlin, so tests are cheap |
| App — Model: Data |
data/**: repositories, DTOs and interceptors, the local cache |
60% | Repositories take their API in the constructor, so fakes reach them. Room and preference code is reached only on a device |
| App — ViewModel | ui/**/*ViewModel.kt |
30% | ViewModels create their own dependencies (HomeViewModel is 3,100 lines), so the JVM can't construct them without Robolectric. Rules are moved into pure state and decision classes and tested there |
| App — View | the rest of ui/**: composables and screen helpers |
15%, measured on a device | Layout is cheaper to check with device tests and manual review; only the key screens get tests |
| App — Platform |
alarm, sensing, background, calendar
|
30% | The decision logic is already pure and tested. Services and receivers need a device and are covered end to end |
| Backend — Computation |
planning, prediction, routing
|
90% | Distributions, learning and route arithmetic decide the alarm time |
| Backend — API |
accounts, events, routines, observations, reports, common
|
85% | Permissions, serialization and validation. The test client reaches them cheaply |
The targets were set on 2026-10-16 after measuring a baseline on 927cfd9, before the Iteration 2
test work began. Layers are assigned per source file by the two coverage_summary.py scripts, so
CI and this page use the same split; a file that fits no layer shows up as "Other".
Excluded from coverage:
-
Backend (
.coveragerc):tests/,migrations/and theseed_*commands that create development data. Onlyapps/is measured, so settings,manage.pyand the verification scripts are outside the count.train_modelsis included because it runs every day in production. -
App (
coverageExcludesinapp/build.gradle.kts): generated code (R,BuildConfig,Manifest, Room*_Impl, ComposeComposableSingletons),ui/theme(color and font tokens) and the entry pointsMainActivityandJitApplication. Composables are not excluded: their low number is real, and the View layer has its own target. -
Test utilities (fakes in
sharedTestandandroidTest, the QA helpers) are not in the main source set, so they are never measured.
How external dependencies are isolated:
-
Backend: the test settings ignore
DATABASE_URLand always use in-memory SQLite, and blank every API key, so code without a fake takes its "no key" path. Tests that need a response replace the lowest HTTP call (urlopen, the Kakao_get, the Seoul_fetch_bytes, the LLM client) with a fake. Each test runs in a transaction that is rolled back. -
App: fake API classes return canned responses or throw
IOExceptionor HTTP errors. Device tests use an in-memory Room database or the app's own one, and log the debug app out on the emulator they run on. - Contract suites: a throwaway SQLite file in the temp folder, never the team's database. The runner refuses to start if port 8000 is busy, so it can't test a stale server.
- Real external calls are checked by hand only, with
backend/scripts/check_external_apis.py.
Environment:
-
Backend: Python 3.12 (CI uses 3.12; 3.13 gives the same results). No
.envis needed; the test settings ignore it. -
App: JDK 25 for the Gradle daemon and JDK 21 as the compile toolchain (CI installs both),
Android SDK platform 37, Python 3 for the summary script. No
local.propertiesis needed beyond the SDK path that Android Studio writes. - Device tests: a running emulator with API 34 or newer, animations off.
# Backend: all tests with coverage; HTML report in backend/htmlcov/index.html
cd backend
pip install -r requirements/dev.txt
python -m pytest --cov --cov-report=term --cov-report=html --cov-report=json
python scripts/coverage_summary.py # coverage per component
python -m pytest -m "not integration" # unit tests only
python -m pytest -m integration # API integration tests only
# App–server contract: 8 HTTP suites against a live local server
python scripts/run_local_suite.py
# App unit tests with coverage; HTML report in
# APP/app/build/reports/jacoco/jacocoDebugUnitTestReport/html/index.html
cd APP
./gradlew jacocoDebugUnitTestReport # runs testDebugUnitTest first
python3 scripts/coverage_summary.py # coverage per layer
# App integration tests on the running emulator
./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=com.swpp.wakeup.integrationOn Windows use .\gradlew.bat and python. The package filter leaves out the end-to-end tests
in the same source folder. CI runs the same commands, plus lint and the debug build; its
per-layer tables are in each run's job summary, and the HTML reports are uploaded as the backend-coverage and app-coverage
artifacts.
- Date: 2026-10-18
-
Commit:
main11a8081 - Run: CI run 68, all four jobs passed
-
Where the numbers come from: test counts from run 68. Backend coverage from run 68, and the
same numbers again in a local run (Linux, Python 3.13). App coverage from the same command run on
a Windows PC at
11a8081(Android Studio's JDK); run 68 runs the same task and prints its table in the job summary - Commands: as in 3.5
| Component | Frameworks | Target | Actual | Date | Command / Report |
|---|---|---|---|---|---|
| App — Model: Domain | JUnit 4, JaCoCo | 80% | 95.6% ✅ | 10/18 |
./gradlew jacocoDebugUnitTestReport → app/build/reports/jacoco
|
| App — Model: Data | JUnit 4, fakes, JaCoCo | 60% | 65.3% ✅ | 10/18 | same |
| App — ViewModel | JUnit 4, JaCoCo | 30% | 8.9% |
10/18 | same |
| App — View | Compose UI Test (device); JaCoCo (JVM) | 15% on a device | ⏳ device coverage not measured; 4.4% on the JVM | 10/18 | same; device: connectedDebugAndroidTest
|
| App — Platform | JUnit 4, JaCoCo | 30% | 11.9% |
10/18 | same |
| Backend — Computation | pytest, pytest-django, pytest-cov | 90% | 93.3% ✅ | 10/18 |
python -m pytest --cov → backend/htmlcov
|
| Backend — API | pytest, pytest-django, pytest-cov | 85% | 95.5% ✅ | 10/18 |
python -m pytest --cov → backend/htmlcov
|
| Suite | Level | Tests | Passed | Failed | Skipped | Time |
|---|---|---|---|---|---|---|
| Backend pytest | Unit 702 · integration 522 | 1,224 | 1,224 | 0 | 0 | 27 s |
| App JVM tests | Unit | 677 | 677 | 0 | 0 | in a 4-minute job with lint and build |
| App device tests (API 34 emulator) | Integration | 33 | 33 | 0 | 0 | 43 s |
| App–server contract (8 suites) | Integration | 268 checks | 234 | 0 | 34 | 11 s |
| End-to-end (2 flows) | End-to-end | — | not run on 11a8081
|
-
App JVM tests: 677 is the number of JUnit
@Testmethods; none is@Ignored, and Gradle fails the job on any failing test. -
Skipped contract checks (34): they look up real routes and need
KAKAO_REST_API_KEY. CI has no key so it never uses up the daily free quota. They run when the key is set inbackend/.env. -
End-to-end: last run 2026-10-17 on
back-iter2d434097(Junha Park): both flows passed in 386 s against a local server with replayed routes.
Device test classes: AuthFlowTest (3), SignupScreenTest (8), AlarmDecisionScreenTest (5),
OfflineFallbackTest (9), CacheDaoTest (8).
| Component | Target | Baseline 10/16 (927cfd9) |
Actual 10/18 (11a8081) |
Status |
|---|---|---|---|---|
| App — Model: Domain | 80% | 86.0% | 95.6% | ✅ |
| App — Model: Data | 60% | 17.0% | 65.3% | ✅ |
| App — ViewModel | 30% | 8.8% | 8.9% | |
| App — View | 15% on a device | 4.0% (JVM) | 4.4% (JVM) | ⏳ |
| App — Platform | 30% | 12.2% | 11.9% | |
| App total (JVM) | 13.3% | 23.2% | ||
| Backend — Computation | 90% | 84.9% | 93.3% | ✅ |
| Backend — API | 85% | 95.0% | 95.5% | ✅ |
| Backend total | 90.1% | 94.7% |
Tests grew from 865 to 1,224 on the backend and from 529 to 677 on the JVM side of the app, and 33 device integration tests were added. Iteration 2 work behind these numbers:
- Coverage tooling, per-layer CI summaries and report artifacts (
927cfd9) - Repository unit tests with fake APIs (
b7b519c, updated for the merged API in73859f8) - Device integration tests and the emulator CI job (
945715a) - Backend unit/integration split with its guard, and tests for
train_modelsand for failures of the external HTTP layer (ee1cda8,11a8081)
Backend coverage report (coverage.py HTML, 11a8081):

Backend test run (summary lines of the same run, with the per-component table and the unit/integration counts):

App coverage report (JaCoCo HTML, JVM unit tests, 11a8081). The per-layer table above
groups the Lines columns by layer; the Cov. columns are instruction and branch coverage:

App JVM test run (Gradle test report of the same run):

App device integration tests (Gradle report from the PC emulator on 2026-10-17, when the tests were written; CI run 68 repeated them on API 34 with the same result, 33 of 33):

App — ViewModel (8.9%, target 30%). The layer is two files, and HomeViewModel alone is
1,622 of its 1,713 executable lines. It creates its repositories itself instead of receiving them, so a JVM
test can't give it fakes; only the rules already moved out of it into pure classes are tested.
Iteration 2 went into the Data layer and device tests instead. AuthFlowTest drives the real
AuthViewModel on the emulator, but device coverage isn't counted yet. Plan for Iteration 3:
pass the repositories into HomeViewModel through its constructor so the fakes in
src/sharedTest can drive it on the JVM, and count device coverage (next item).
App — View (not measured on a device). The target is defined on device coverage, and
enableAndroidTestCoverage is not on yet, so we only have the JVM number (4.4%). Plan for
Iteration 3: turn it on and have the emulator CI job publish the device report.
App — Platform (11.9%, target 30%). The tested parts are the pure rules (TripGeofence
97.5%, TripLiveState 87.2%). Most of the rest runs inside Android components and is at 0%:
TripTrackingService (326 lines), AlarmActivity (163) and AlarmScheduler (73). The upload
queues are different: TripObservationQueue, BlockObservationQueue and AlarmLogQueue (212
lines together, 7% covered) only store a list in SharedPreferences, but they open it from a
Context, which a JVM test doesn't have. Plan for Iteration 3: let the three queues take
SharedPreferences so the existing InMemoryPrefs fake can drive them on the JVM, and test
AlarmScheduler on the emulator. Covering those files, about 280 lines, would reach the target.
Backend, both components above target. The largest uncovered pieces:
-
accounts/management/commands/backfill_profiles.py(0%): a one-off data migration command. -
routing/clients.py(90.0%):transit_minutesandwalk_minutesare no longer called by production code. They are candidates for deletion rather than for new tests.
To be completed in Iteration 3.
Plan to be completed in Iteration 3; results in Iteration 4. The Iteration 1 page maps the current acceptance tests to automated tests (section 5).
To be completed in Iteration 5.