team18‐iter2‐testing - snuhcs-course/swpp-2026-project-team-18 GitHub Wiki

JustInTime — Testing Documentation (Iteration 2)

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.

1. Revision History

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 · ⚠️ below target · ⏳ not measured yet


2. Test Responsibility & Schedule

2.1 Who

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

  1. 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).
  2. A bug fix comes with a regression test that fails without the fix.
  3. 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.
  4. Backend tests that send a request through the Django test client carry @pytest.mark.integration. backend/conftest.py fails any test whose marker doesn't match what it actually did, so the unit and integration counts below can't drift.
  5. No test reaches the shared server, the shared database or a paid external API. CI has no secrets.

2.2 When

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"]
Loading

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.


3. Unit & Integration Testing — Plan

3.1 Test levels

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

3.2 Frameworks and tools

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.

3.3 Coverage targets by architectural layer

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".

3.4 Scope, exclusions and isolation

Excluded from coverage:

  • Backend (.coveragerc): tests/, migrations/ and the seed_* commands that create development data. Only apps/ is measured, so settings, manage.py and the verification scripts are outside the count. train_models is included because it runs every day in production.
  • App (coverageExcludes in app/build.gradle.kts): generated code (R, BuildConfig, Manifest, Room *_Impl, Compose ComposableSingletons), ui/theme (color and font tokens) and the entry points MainActivity and JitApplication. Composables are not excluded: their low number is real, and the View layer has its own target.
  • Test utilities (fakes in sharedTest and androidTest, 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_URL and 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 IOException or 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.

3.5 Reproducible commands

Environment:

  • Backend: Python 3.12 (CI uses 3.12; 3.13 gives the same results). No .env is 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.properties is 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.integration

On 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.


4. Unit & Integration Testing — Results (Iteration 2)

  • Date: 2026-10-18
  • Commit: main 11a8081
  • 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

4.1 Summary

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

4.2 Tests run

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 @Test methods; 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 in backend/.env.
  • End-to-end: last run 2026-10-17 on back-iter2 d434097 (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).

4.3 Coverage against targets

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 in 73859f8)
  • Device integration tests and the emulator CI job (945715a)
  • Backend unit/integration split with its guard, and tests for train_models and for failures of the external HTTP layer (ee1cda8, 11a8081)

Backend coverage report (coverage.py HTML, 11a8081):

Backend coverage report, 94.7% overall

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

Backend pytest output: 1224 passed, 702 unit and 522 integration

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 JaCoCo report, 3,039 of 13,098 lines covered

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

App unit tests: 677 tests, 0 failures, 0 skipped

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 device integration tests: 33 tests, 0 failures

4.4 Below target, and what we will do

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_minutes and walk_minutes are no longer called by production code. They are candidates for deletion rather than for new tests.

5. AI Module Testing

To be completed in Iteration 3.

6. Acceptance Testing

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).

7. Alpha & Beta Testing

To be completed in Iteration 5.

⚠️ **GitHub.com Fallback** ⚠️