Skip to content

test: Phase 3 — Mockito migration, fakes infrastructure, and coverage gaps#3539

Merged
ahmedre merged 26 commits intoquran:mainfrom
ksalhab89:feature/testing-phase2
Mar 27, 2026
Merged

test: Phase 3 — Mockito migration, fakes infrastructure, and coverage gaps#3539
ahmedre merged 26 commits intoquran:mainfrom
ksalhab89:feature/testing-phase2

Conversation

@ksalhab89
Copy link
Copy Markdown
Contributor

@ksalhab89 ksalhab89 commented Feb 18, 2026

Summary

Phase 3: Presenter tests, Mockito migration, fakes infrastructure, and coverage gaps.

Builds on Phase 2 (#3538).

New Tests

Presenter Tests (moved from Phase 2 — now use Robolectric)

  • QuranPagePresenter — 8 tests: page loading, coordinates, image download, lifecycle
  • AudioPresenter — 13 tests: playback, streaming, download triggers, permissions, gapless/gapped

Mockito Migration

  • BookmarkModel, RecentPageModel → in-memory SQLite + fakes
  • TagBookmarkPresenter → FakeBookmarkModel, FakeRecentPageModel
  • QuranImportPresenter → Robolectric + ShadowContentResolver
  • BaseTranslationPresenter → FakeTranslationModel
  • ArabicDatabaseUtils, AudioUtils, BookmarkImportExportModel → Robolectric

Coverage Gaps Closed

  • AudioPresenter download paths (aya position, gapless DB, basmallah, full range, permission flow)
  • BaseTranslationPresenter.getVerses() coroutine exception paths
  • BookmarkImportExportModel export path

Production Changes

Interface Extractions (for testability)

  • TranslationModel → interface + TranslationModelImpl
  • TranslationsDBAdapter → interface + TranslationsDBAdapterImpl
  • TranslationListPresenter → interface + TranslationListPresenterImpl

BookmarksDao.changes: Flow<Long> → Flow<Unit>

  • Timestamps were never consumed by callers — only used as a change signal
  • Removes flaky timestamp-comparison risk in tests
  • Fixes pre-existing double-emit in deleteBookmarkById (was masked by StateFlow conflation)

Code Quality

  • Extract inMemoryBookmarksAdapter() to shared helper
  • Fix !!requireNotNull() in BookmarkModelTest
  • Fix getBooleanExtra wrong default in AudioPresenterTest
  • Fix JDBC classloader flakiness via Robolectric sandbox isolation
  • Delete 7 unused fakes (never imported by any test)

Test Plan

  • ./gradlew :app:testMadaniDebugUnitTest — all tests pass, 0 failures (2 consecutive runs)
  • ./gradlew :common:bookmark:testReleaseUnitTest — pass
  • ./gradlew :feature:audio:testReleaseUnitTest :common:audio:testReleaseUnitTest — pass

@github-actions
Copy link
Copy Markdown

OLD: app-madani-debug.apk (signature: V1, V2)
NEW: app-madani-debug.apk (signature: V1, V2)

          │            compressed            │           uncompressed           
          ├───────────┬───────────┬──────────┼───────────┬───────────┬──────────
 APK      │ old       │ new       │ diff     │ old       │ new       │ diff     
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
      dex │  20.2 MiB │  20.2 MiB │ +2.5 KiB │  51.2 MiB │  51.3 MiB │ +9.1 KiB 
     arsc │   2.1 MiB │   2.1 MiB │      0 B │   2.1 MiB │   2.1 MiB │      0 B 
 manifest │   5.7 KiB │   5.7 KiB │      0 B │  26.6 KiB │  26.6 KiB │      0 B 
      res │   1.8 MiB │   1.8 MiB │      0 B │     2 MiB │     2 MiB │      0 B 
   native │    91 KiB │  86.1 KiB │ -4.9 KiB │  36.5 KiB │  36.5 KiB │      0 B 
    asset │   1.8 MiB │   1.8 MiB │      0 B │   4.1 MiB │   4.1 MiB │      0 B 
    other │ 200.1 KiB │ 200.1 KiB │    +17 B │ 501.8 KiB │ 501.8 KiB │      0 B 
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
    total │  26.2 MiB │  26.2 MiB │ -2.5 KiB │    60 MiB │    60 MiB │ +9.1 KiB 

@ahmedre ahmedre added this pull request to the merge queue Mar 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Mar 27, 2026
@ahmedre ahmedre added this pull request to the merge queue Mar 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Mar 27, 2026
@ahmedre ahmedre added this pull request to the merge queue Mar 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Mar 27, 2026
Document the 'fakes over mocks' pattern with:
- Why shared fakes don't work (circular dependencies)
- Correct pattern: local fakes in test source sets
- Concrete examples for QuranSettings
- Migration strategy for Week 3-4 presenter tests
- Key principles and best practices

This guide will serve as reference for implementing presenter tests
in Phase 2 Week 3-4.
Tests cover:
- Screen binding/unbinding lifecycle
- Page coordinates loading with shouldOverlayPageInfo setting
- Ayah coordinates loading after page coordinates
- Image downloading on first bind
- Invalid page filtering
- Error handling for coordinates
- Disposable cleanup on unbind

Uses Mockito for framework-dependent QuranSettings (private constructor).
Other dependencies mocked to focus on presenter logic.

All 9 tests passing.
Tests cover:
- Basic playback when files are available
- Streaming mode when files missing
- Streaming override when files are available
- Start/End ayah swapping when reversed
- Permission callbacks (download granted, download success)
- Lifecycle management (bind/unbind)

Note: Download tests requiring Intent creation are deferred (need Robolectric setup).
Core business logic is validated.

Added test-utils dependency to app module for TestDataFactory access.

All 8 tests passing.
Phase 1 Complete:
- QuranInfo tests (21 tests)
- BookmarksDaoImpl tests (16 tests)
- Test infrastructure (TestDataFactory, RxSchedulerRule)
- Fakes-over-mocks pattern established

Phase 2 Complete:
- QuranPagePresenter tests (9 tests)
- AudioPresenter tests (8 tests)
- Pragmatic mocking strategy documented
- Total: 33 new tests, all passing

Updated roadmap to show completed phases and document key decisions.
Replace @BeforeClass/@afterclass RxAndroidPlugins setup with
RxSchedulerRule to ensure proper cleanup after each test.

This prevents scheduler state from leaking between tests
when running the full test suite.

All 33 Phase 1 & 2 tests still passing.
Analyzed 10 pre-existing test files using Mockito to identify
migration candidates for fakes-over-mocks strategy.

High-Priority Candidates:
- BookmarkModelTest (13 mocks) → in-memory SQLite
- RecentPageModelTest (12 mocks) → in-memory SQLite
- BookmarkPresenterTest (14 mocks) → FakeBookmarkModel

Target: Reduce Mockito files from 10 to 6-7 (50% reduction)

Strategy:
- Week 1: Migrate database tests to in-memory SQLite
- Week 2: Create FakeBookmarkModel and migrate presenter tests
- Week 3: Evaluate remaining files, document decisions

Documented patterns, metrics, and implementation guidance.
Removed Thread.sleep(600) from async timer test. The RxSchedulerRule's
trampoline scheduler executes Completable.timer() immediately without
waiting for the actual 500ms delay, making Thread.sleep unnecessary
and potentially flaky.

Changes:
- Removed Thread.sleep(600) from line 148
- Added comment explaining trampoline behavior
- All 126 tests pass with fix applied
- Delete PHASE2_FAKES_GUIDE.md and PHASE3_MOCKITO_MIGRATION.md (planning docs)
- Reset TESTING_STRATEGY.md to upstream version (remove roadmap bloat)
- Remove no-op tearDown in BookmarksDaoImplTest
- Fix zero-assertion unbind test in QuranPagePresenterTest (now verifies
  screen does not receive emissions after unbind)
- Replace fully-qualified Mockito references with imports
- Fix any() null crash in unbind test: use specific mock instance instead
- Fix flaky timestamp comparison in removal notification test
Added test dependencies required for in-memory SQLite testing:
- sqldelight-sqlite-driver: JVM SQLite driver for in-memory DBs
- sqldelight-primitive-adapters: Int column adapters for BookmarksDatabase
…-memory SQLite

Replace Mockito-based Java tests with real in-memory SQLite implementations:
- BookmarkModelTest: 2 tests using JdbcSqliteDriver.IN_MEMORY
- RecentPageModelTest: 6 tests using JdbcSqliteDriver.IN_MEMORY
- Fix @BeforeClass test pollution → use RxSchedulerRule per test
- Zero Mockito usage in these test files
BookmarkPresenterTest: Replace @BeforeClass RxAndroidPlugins with RxSchedulerRule
to prevent scheduler state from leaking between test classes.

TagBookmarkPresenterTest: Remove spy() and unnecessary MockitoAnnotations,
keeping only minimal mock() for Android framework dependencies.
Add stateful fakes to replace Mockito mocks in future tests:
- FakeBookmarksDBAdapter: In-memory bookmark database adapter
- FakeBookmarkModel / FakeRecentPageModel: Business logic fakes
- FakeQariUtil, FakeQuranFileUtils: Utility fakes
- FakeTranslationModel, FakeTranslationsDBAdapter, FakeTranslationListPresenter: Translation fakes
- FakeContentResolver: Content resolver fake
- UI fakes: FakeQuranPageScreen, FakePagerActivity, FakeTagBookmarkDialog,
  FakeCoordinatesModel, FakeQuranPageLoader, FakeAudioExtensionDecider, FakeQuranDisplayData
- Test helpers: ContextTestHelpers, ResourcesTestHelpers, DatabaseHandlerTestHelpers
- TagBookmarkPresenterTest: Replace @BeforeClass RxAndroidPlugins with
  RxSchedulerRule to match all other test files and prevent scheduler
  state from leaking between test classes
- RecentPageModelTest: Make SAMPLE_RECENT_PAGES immutable (listOf)
  to prevent accidental future mutation of companion object state
- FakeBookmarkModel/FakeRecentPageModel: Document why passing a Mockito
  mock to the real constructor is safe (constructor stores but never
  calls methods on the adapter)
…fakes

FakeBookmarkModel and FakeRecentPageModel were passing mock(BookmarksDBAdapter)
to the real superclass constructor. Replace with a real BookmarksDBAdapter backed
by an in-memory SQLite database (same JdbcSqliteDriver.IN_MEMORY pattern used
in BookmarkModelTest). The fakes are now true fakes with zero Mockito dependency.
…RecentPageModel

- AudioUtilsTest: extract shared audioUtils() helper, remove dead pageProviderMock
  vars; document QuranFileUtils/QariUtil as constructor fillers until Robolectric added
- TagBookmarkPresenterTest: replace mock(BookmarksDBAdapter) with inMemoryBookmarksAdapter()
  and mock(RecentPageModel) with FakeRecentPageModel() across all 3 anonymous subclasses;
  retain mock(TagBookmarkDialog) — extends DialogFragment, needs Robolectric to instantiate
- FakeRecentPageModel: fix JVM init-order NPE: RecentPageModel.init calls
  getRecentPagesObservable() before subclass fields are initialized; switch recentPages
  and getRecentPagesObservableCalls to lazy-init backing vars so they survive the
  pre-init window safely
ksalhab89 and others added 10 commits March 28, 2026 00:29
…PageProvider

A4: FakePageProvider implements PageProvider (17 abstract methods)
    Enables QariUtil(FakePageProvider()) without mocks
    AudioUtilsTest migrated to Robolectric — 0 Mockito imports

A2: Extract interfaces from 3 non-open concrete classes (DIP)
    TranslationModel → interface + TranslationModelImpl
    TranslationsDBAdapter → interface + TranslationsDBAdapterImpl
    TranslationListPresenter → interface + TranslationListPresenterImpl
    Metro DI bindings added in DatabaseModule + PagerActivityModule
    FakeTranslationModel, FakeTranslationsDBAdapter implement interfaces
    FakeTranslationListPresenter uses production interface
    BaseTranslationPresenterTest — 0 Mockito imports

A1: Robolectric for Android context dependencies
    BookmarkImportExportModelTest.java → .kt (FakeBookmarkModel, real context)
    ArabicDatabaseUtilsTest: real Robolectric context replaces mock(Context)
    2 remaining mocks: DatabaseHandler (private ctor), QuranFileUtils (complex ctor)
…tial QuranImportPresenter migration

ArabicDatabaseUtilsTest: replace @mock QuranFileUtils with a real instance
constructed via Robolectric context + FakePageProvider + QuranScreenInfo (same
pattern established in AudioUtilsTest). DatabaseHandler mock remains — it has
a private constructor and is non-open; it is returned from an override and
never interacted with.

QuranImportPresenterTest (A3): add Robolectric runner + Config; replace
mock(Context)/mock(Uri)/mock(BookmarkModel) with real Robolectric context,
Uri.parse(), and FakeBookmarkModel(). Use ShadowContentResolver for
testParseExternalFile. Three tests still require Mockito ContentResolver +
Context because ShadowContentResolver 4.16.1 does not shadow openFileDescriptor
and returns a non-null sentinel for unregistered URIs rather than null;
ParcelFileDescriptor mock also remains for the null-fd NPE path.

All 4 QuranImportPresenterTest and 6 ArabicDatabaseUtilsTest tests pass.
…ode quality

Fix pre-existing flaky failures (BookmarkModelTest, RecentPageModelTest,
TagBookmarkPresenterTest) caused by Robolectric's sandbox classloader intercepting
the SQLite JDBC driver before plain JUnit tests could access it via DriverManager.
Fix: add @RunWith(RobolectricTestRunner::class) to all SQLite-backed tests so every
test uses the same classloader context. Full suite now passes in back-to-back runs.

Fix LongArray equality semantics: UpdateTagsCall (FakeBookmarkModel) and
TagBookmarksCall (FakeBookmarksDBAdapter) used LongArray in data classes which
breaks equals/hashCode. Changed to List<Long> and removed the contentEquals()
workaround in assertion methods.

Code quality:
- Remove dead FakeContentResolver (never used in any test)
- Rename getGetBookmarkTagIdsCallCount() -> getBookmarkTagIdsCallCount()
- Rename getGetRecentPagesObservableCallCount() -> getRecentPagesObservableCallCount()
- ArrayList<Bookmark>() -> mutableListOf() in ArabicDatabaseUtilsTest
- Anonymous HashMap init block -> mapOf() in BaseTranslationPresenterTest
- ArrayList() -> emptyList() in BaseTranslationPresenterTest
- Add comments explaining the deprecated Display API constraint in tests
…ad test files

- Add @RunWith(RobolectricTestRunner) to AudioPresenterTest (needed for Intent creation)
- Add 5 new tests covering all download intent paths:
  - Path A: aya position file missing -> handleRequiredDownload
  - Path B: gapless database file missing -> handleRequiredDownload
  - Path C: basmallah download -> single-verse intent (start == end)
  - Path D: full range download -> start/end/isGapless extras verified
  - Path E: onPostNotificationsPermissionResponse -> proceedWithDownload (bypass)
- Delete 5 dead files: FakeQariUtil, FakeQuranFileUtils, ExampleUsage (unused fakes/stubs),
  BookmarkModelTest.java, RecentPageModelTest.java (commented-out stubs with Kotlin equivalents)
…add coverage

- Extract inMemoryBookmarksAdapter() to DatabaseTestHelpers.kt (3 duplicates removed)
- Fix AudioPresenterTest: getBooleanExtra default true→false (masks bug when extra absent)
- Fix BookmarkModelTest: !! force-unwrap → requireNotNull() (idiomatic Kotlin)
- Add @RunWith/@config to BaseTranslationPresenterTest (was missing Robolectric context)
- Add getVerses error path tests to BaseTranslationPresenterTest (covers getVerses$2 lambda)
- Add FakeTranslationModel error injection support (setArabicError/setTranslationError)
- Add export path tests to BookmarkImportExportModelTest (covers exportBookmarks* methods)
QariItem gained opusUrl parameter, shifting positional args in AudioUpdaterTest.
GaplessAudioInfoCommand and GappedAudioInfoCommand gained allowedExtensions
parameter, breaking calls in audio info command tests.
- Delete PHASE2_ANDROID_FRAMEWORK_ANALYSIS.md, PHASE2_EXPERT_REVIEW_SUMMARY.md
- Delete fakes/README.md, FOUNDATION_FAKES.md, ANDROID_FRAMEWORK_FAKES.md
- Delete test/fakes/README.md, test/helpers/README.md
- Delete 7 unused fakes in test/fakes/ (zero imports from any test)
- Fix tautological permission tests: now exercise download→permission→replay flow
- Fix FakeTranslationsDBAdapter to remove unavailable translations (matches production)
- Fix broken doReturn import
- Remove unused AudioPathInfo import
- Fix Kotlin nullability issue with Mockito any() in verify calls
…ow<Unit>

Timestamps were never consumed by callers — only used as a change signal.
Switching to Unit removes flaky timestamp-comparison risk in tests and
simplifies the API. Also fixes a pre-existing double-emit in
deleteBookmarkById (masked by StateFlow conflation).
@ahmedre ahmedre force-pushed the feature/testing-phase2 branch from 9a74809 to 2656075 Compare March 27, 2026 20:31
@ahmedre ahmedre enabled auto-merge March 27, 2026 20:33
@ahmedre ahmedre disabled auto-merge March 27, 2026 20:34
@github-actions
Copy link
Copy Markdown

OLD: app-madani-debug.apk (signature: V1, V2)
NEW: app-madani-debug.apk (signature: V1, V2)

          │            compressed            │           uncompressed           
          ├───────────┬───────────┬──────────┼───────────┬───────────┬──────────
 APK      │ old       │ new       │ diff     │ old       │ new       │ diff     
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
      dex │  20.2 MiB │  20.2 MiB │ +2.5 KiB │  51.2 MiB │  51.3 MiB │ +9.1 KiB 
     arsc │   2.1 MiB │   2.1 MiB │      0 B │   2.1 MiB │   2.1 MiB │      0 B 
 manifest │   5.7 KiB │   5.7 KiB │      0 B │  26.6 KiB │  26.6 KiB │      0 B 
      res │   1.8 MiB │   1.8 MiB │      0 B │     2 MiB │     2 MiB │      0 B 
   native │    91 KiB │  86.1 KiB │ -4.9 KiB │  36.5 KiB │  36.5 KiB │      0 B 
    asset │   1.8 MiB │   1.8 MiB │      0 B │   4.1 MiB │   4.1 MiB │      0 B 
    other │ 200.1 KiB │ 200.1 KiB │    +13 B │ 501.8 KiB │ 501.8 KiB │      0 B 
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
    total │  26.2 MiB │  26.2 MiB │ -2.5 KiB │    60 MiB │    60 MiB │ +9.1 KiB 

@ahmedre ahmedre added this pull request to the merge queue Mar 27, 2026
Merged via the queue into quran:main with commit 7b9b711 Mar 27, 2026
2 checks passed
@ksalhab89 ksalhab89 deleted the feature/testing-phase2 branch March 29, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants