Skip to content

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

Open
ksalhab89 wants to merge 21 commits intoquran:mainfrom
ksalhab89:feature/testing-phase2
Open

test: Phase 3 — Mockito migration, fakes infrastructure, and coverage gaps#3539
ksalhab89 wants to merge 21 commits intoquran:mainfrom
ksalhab89:feature/testing-phase2

Conversation

@ksalhab89
Copy link
Contributor

Summary

Phase 3 of the testing infrastructure. Migrates remaining Mockito-heavy tests to fakes and Robolectric, closes coverage gaps, and fixes pre-existing test breaks from upstream API changes.

Stacked on Phase 2: see Phase 2 PR. The incremental diff for this PR starts at commit cdd17bba (above feature/testing-phase2-presenters). To review only Phase 3 changes: git diff feature/testing-phase2-presenters...feature/testing-phase2

Changes

Mockito Migration

Migrated all feasible tests away from Mockito mocks to real fakes or Robolectric:

  • BookmarkModel, RecentPageModel → in-memory SQLite + fakes
  • TagBookmarkPresenterFakeBookmarkModel, FakeRecentPageModel
  • QuranImportPresenter → Robolectric + ShadowContentResolver
  • BaseTranslationPresenterFakeTranslationModel
  • ArabicDatabaseUtils, AudioUtils, BookmarkImportExportModel → Robolectric real context

Remaining Mockito usage is justified: verify() interaction tests or non-open classes.

Fakes Created

FakeBookmarkModel, FakeRecentPageModel, FakeTranslationModel, FakeTranslationsDBAdapter, FakeTranslationListPresenter, FakePageProvider, FakeBookmarksDBAdapter

Interface Extractions (for testability)

TranslationModel, TranslationsDBAdapter, TranslationListPresenter

Coverage Gaps Closed

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

Code Quality

  • Extract inMemoryBookmarksAdapter() to DatabaseTestHelpers.kt (removes 3 duplicates)
  • Fix !! force-unwrap → requireNotNull() in BookmarkModelTest
  • Fix getBooleanExtra wrong default in AudioPresenterTest (masked bug when extra absent)
  • Add @RunWith/@Config to BaseTranslationPresenterTest
  • Fix JDBC classloader flakiness via Robolectric sandbox isolation

Pre-existing Test Fixes (upstream API changes)

  • AudioUpdaterTestQariItem.opusUrl added upstream; updated to named params
  • GaplessAudioInfoCommandTest / GappedAudioInfoCommandTestallowedExtensions param added to download query methods

Phase 3 Commits (above Phase 2)

814b3497 test: Fix pre-existing test compilation breaks from upstream sync
62d89c84 test: Fix expert review issues - extract helper, remove code smells, add coverage
2065e06c test(audio): Add download path coverage for AudioPresenter, remove dead test files
77614bf8 fix(tests): Fix JDBC classloader flakiness, LongArray equality, and code quality
58df929f refactor(tests): A1-cleanup + A3 — eliminate QuranFileUtils mock, partial QuranImportPresenter migration
74bcaea7 refactor(tests): A1/A2/A4 — Robolectric + interface extraction + FakePageProvider
34f5fbd2 refactor(tests): Wire up fakes in Group B, fix init-order NPE in FakeRecentPageModel
9e021384 refactor(fakes): Replace Mockito mock with real in-memory adapter in fakes
1de97836 fix(tests): Address review findings from multi-agent audit
1a743dee test(fakes): Add fake implementations for test doubles infrastructure
03acf2bc fix(tests): Fix test pollution and reduce Mockito in presenter tests
e7b7e7db test(bookmark): Migrate BookmarkModel and RecentPageModel tests to in-memory SQLite
cdd17bba test: Add SQLite in-memory driver dependencies for database tests

Test Plan

  • ./gradlew :app:testMadaniDebugUnitTest — 146 tests, 0 failures, 0 flaky (2 consecutive runs)
  • ./gradlew :common:data:test :common:bookmark:test — pass
  • ./gradlew :feature:audio:testReleaseUnitTest :common:audio:testReleaseUnitTest — pass
  • ./gradlew :app:lintMadaniDebug — 0 errors

- Add sqldelight-sqlite-driver for in-memory database testing
- Test all DAO operations: bookmarks, toggles, recent pages
- Test Flow emissions and change notifications
- Use real BookmarksDatabase with proper column adapters
- All tests use Turbine for Flow testing and Truth for assertions

Phase 2 Week 1 complete - exceeded 15 test target with 16 tests
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
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
…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.
@ksalhab89 ksalhab89 force-pushed the feature/testing-phase2 branch from 814b349 to 5fcf46f Compare February 23, 2026 08:08
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.

1 participant