From 0902b0ba49a580508ab770d2f7b61cc0fd53178e Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 12 Oct 2023 11:22:41 +0200 Subject: [PATCH] refactor: Replace test preference mocks with InMemorySharedPreferences (#155) Previously the tests mocked shared preferences with a map and a mock that had to be implemented for each test that needed it. Replace this with `InMemorySharedPreferences`, which provides the normal `SharedPreferences` interface so can be used as a drop-in replacement. --- .../NotificationsViewModelTestBase.kt | 32 ++-- ...ationsViewModelTestStatusDisplayOptions.kt | 9 +- .../NotificationsViewModelTestUiState.kt | 5 +- .../CachedTimelineViewModelTestBase.kt | 30 ++-- ...melineViewModelTestStatusDisplayOptions.kt | 9 +- .../CachedTimelineViewModelTestUiState.kt | 5 +- .../NetworkTimelineViewModelTestBase.kt | 30 ++-- ...melineViewModelTestStatusDisplayOptions.kt | 9 +- .../NetworkTimelineViewModelTestUiState.kt | 5 +- .../pachli/fakes/InMemorySharedPreferences.kt | 140 ++++++++++++++++++ 10 files changed, 212 insertions(+), 62 deletions(-) create mode 100644 app/src/test/java/app/pachli/fakes/InMemorySharedPreferences.kt diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt index 2011248d1..f649d63e3 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt @@ -25,6 +25,7 @@ import app.pachli.components.timeline.FiltersRepository import app.pachli.components.timeline.MainCoroutineRule import app.pachli.db.AccountEntity import app.pachli.db.AccountManager +import app.pachli.fakes.InMemorySharedPreferences import app.pachli.network.FilterModel import app.pachli.settings.PrefKeys import app.pachli.usecase.TimelineCases @@ -33,8 +34,6 @@ import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.robolectric.Shadows.shadowOf @@ -44,7 +43,6 @@ import retrofit2.Response @RunWith(AndroidJUnit4::class) abstract class NotificationsViewModelTestBase { protected lateinit var notificationsRepository: NotificationsRepository - protected lateinit var sharedPreferencesMap: MutableMap protected lateinit var sharedPreferences: SharedPreferences protected lateinit var accountManager: AccountManager protected lateinit var timelineCases: TimelineCases @@ -71,24 +69,20 @@ abstract class NotificationsViewModelTestBase { notificationsRepository = mock() - // Backing store for sharedPreferences, to allow mutation in tests - sharedPreferencesMap = mutableMapOf( - PrefKeys.ANIMATE_GIF_AVATARS to false, - PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, - PrefKeys.ABSOLUTE_TIME_VIEW to false, - PrefKeys.SHOW_BOT_OVERLAY to true, - PrefKeys.USE_BLURHASH to true, - PrefKeys.CONFIRM_REBLOGS to true, - PrefKeys.CONFIRM_FAVOURITES to false, - PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, - PrefKeys.FAB_HIDE to false, + sharedPreferences = InMemorySharedPreferences( + mapOf( + PrefKeys.ANIMATE_GIF_AVATARS to false, + PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, + PrefKeys.ABSOLUTE_TIME_VIEW to false, + PrefKeys.SHOW_BOT_OVERLAY to true, + PrefKeys.USE_BLURHASH to true, + PrefKeys.CONFIRM_REBLOGS to true, + PrefKeys.CONFIRM_FAVOURITES to false, + PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, + PrefKeys.FAB_HIDE to false, + ), ) - // Any getBoolean() call looks for the result in sharedPreferencesMap - sharedPreferences = mock { - on { getBoolean(any(), any()) } doAnswer { sharedPreferencesMap[it.arguments[0]] } - } - accountManager = mock { on { activeAccount } doReturn AccountEntity( id = 1, diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusDisplayOptions.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusDisplayOptions.kt index 5f10f6f7e..6bebbdc1f 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusDisplayOptions.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusDisplayOptions.kt @@ -17,6 +17,7 @@ package app.pachli.components.notifications +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.settings.PrefKeys @@ -65,7 +66,9 @@ class NotificationsViewModelTestStatusDisplayOptions : NotificationsViewModelTes assertThat(defaultStatusDisplayOptions.animateAvatars).isFalse() // Given; just a change to one preferences - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When val updatedOptions = defaultStatusDisplayOptions.make( @@ -87,7 +90,9 @@ class NotificationsViewModelTestStatusDisplayOptions : NotificationsViewModelTes } // Given - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.ANIMATE_GIF_AVATARS)) diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestUiState.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestUiState.kt index f39383f05..d2fadeeb2 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestUiState.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestUiState.kt @@ -17,6 +17,7 @@ package app.pachli.components.notifications +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.entity.Notification @@ -53,7 +54,9 @@ class NotificationsViewModelTestUiState : NotificationsViewModelTestBase() { } // Given - sharedPreferencesMap[PrefKeys.FAB_HIDE] = true + sharedPreferences.edit { + putBoolean(PrefKeys.FAB_HIDE, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.FAB_HIDE)) diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt index f34feb3c6..736617767 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt @@ -26,6 +26,7 @@ import app.pachli.components.timeline.viewmodel.CachedTimelineViewModel import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.db.AccountEntity import app.pachli.db.AccountManager +import app.pachli.fakes.InMemorySharedPreferences import app.pachli.network.FilterModel import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.PrefKeys @@ -50,7 +51,6 @@ import retrofit2.Response @RunWith(AndroidJUnit4::class) abstract class CachedTimelineViewModelTestBase { protected lateinit var cachedTimelineRepository: CachedTimelineRepository - protected lateinit var sharedPreferencesMap: MutableMap protected lateinit var sharedPreferences: SharedPreferences protected lateinit var accountPreferencesMap: MutableMap protected lateinit var accountPreferenceDataStore: AccountPreferenceDataStore @@ -79,24 +79,20 @@ abstract class CachedTimelineViewModelTestBase { cachedTimelineRepository = mock() - // Backing store for sharedPreferences, to allow mutation in tests - sharedPreferencesMap = mutableMapOf( - PrefKeys.ANIMATE_GIF_AVATARS to false, - PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, - PrefKeys.ABSOLUTE_TIME_VIEW to false, - PrefKeys.SHOW_BOT_OVERLAY to true, - PrefKeys.USE_BLURHASH to true, - PrefKeys.CONFIRM_REBLOGS to true, - PrefKeys.CONFIRM_FAVOURITES to false, - PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, - PrefKeys.FAB_HIDE to false, + sharedPreferences = InMemorySharedPreferences( + mapOf( + PrefKeys.ANIMATE_GIF_AVATARS to false, + PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, + PrefKeys.ABSOLUTE_TIME_VIEW to false, + PrefKeys.SHOW_BOT_OVERLAY to true, + PrefKeys.USE_BLURHASH to true, + PrefKeys.CONFIRM_REBLOGS to true, + PrefKeys.CONFIRM_FAVOURITES to false, + PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, + PrefKeys.FAB_HIDE to false, + ), ) - // Any getBoolean() call looks for the result in sharedPreferencesMap - sharedPreferences = mock { - on { getBoolean(any(), any()) } doAnswer { sharedPreferencesMap[it.arguments[0]] } - } - // Backing store for account preferences, to allow mutation in tests accountPreferencesMap = mutableMapOf( PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA to false, diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusDisplayOptions.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusDisplayOptions.kt index 8bf232a19..5b132b1d5 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusDisplayOptions.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusDisplayOptions.kt @@ -17,6 +17,7 @@ package app.pachli.components.timeline +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.settings.PrefKeys @@ -67,7 +68,9 @@ class CachedTimelineViewModelTestStatusDisplayOptions : CachedTimelineViewModelT assertThat(defaultStatusDisplayOptions.animateAvatars).isFalse() // Given; just a change to one preferences - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When val updatedOptions = defaultStatusDisplayOptions.make( @@ -89,7 +92,9 @@ class CachedTimelineViewModelTestStatusDisplayOptions : CachedTimelineViewModelT } // Given - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.ANIMATE_GIF_AVATARS)) diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestUiState.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestUiState.kt index e81e4584c..7193e4052 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestUiState.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestUiState.kt @@ -17,6 +17,7 @@ package app.pachli.components.timeline +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.components.timeline.viewmodel.UiState @@ -53,7 +54,9 @@ class CachedTimelineViewModelTestUiState : CachedTimelineViewModelTestBase() { } // Given - sharedPreferencesMap[PrefKeys.FAB_HIDE] = true + sharedPreferences.edit { + putBoolean(PrefKeys.FAB_HIDE, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.FAB_HIDE)) diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt index ec406f53d..449050ae9 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt @@ -26,6 +26,7 @@ import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.db.AccountEntity import app.pachli.db.AccountManager +import app.pachli.fakes.InMemorySharedPreferences import app.pachli.network.FilterModel import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.PrefKeys @@ -49,7 +50,6 @@ import retrofit2.Response @RunWith(AndroidJUnit4::class) abstract class NetworkTimelineViewModelTestBase { protected lateinit var networkTimelineRepository: NetworkTimelineRepository - protected lateinit var sharedPreferencesMap: MutableMap protected lateinit var sharedPreferences: SharedPreferences protected lateinit var accountPreferencesMap: MutableMap protected lateinit var accountPreferenceDataStore: AccountPreferenceDataStore @@ -78,24 +78,20 @@ abstract class NetworkTimelineViewModelTestBase { networkTimelineRepository = mock() - // Backing store for sharedPreferences, to allow mutation in tests - sharedPreferencesMap = mutableMapOf( - PrefKeys.ANIMATE_GIF_AVATARS to false, - PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, - PrefKeys.ABSOLUTE_TIME_VIEW to false, - PrefKeys.SHOW_BOT_OVERLAY to true, - PrefKeys.USE_BLURHASH to true, - PrefKeys.CONFIRM_REBLOGS to true, - PrefKeys.CONFIRM_FAVOURITES to false, - PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, - PrefKeys.FAB_HIDE to false, + sharedPreferences = InMemorySharedPreferences( + mapOf( + PrefKeys.ANIMATE_GIF_AVATARS to false, + PrefKeys.ANIMATE_CUSTOM_EMOJIS to false, + PrefKeys.ABSOLUTE_TIME_VIEW to false, + PrefKeys.SHOW_BOT_OVERLAY to true, + PrefKeys.USE_BLURHASH to true, + PrefKeys.CONFIRM_REBLOGS to true, + PrefKeys.CONFIRM_FAVOURITES to false, + PrefKeys.WELLBEING_HIDE_STATS_POSTS to false, + PrefKeys.FAB_HIDE to false, + ), ) - // Any getBoolean() call looks for the result in sharedPreferencesMap - sharedPreferences = mock { - on { getBoolean(any(), any()) } doAnswer { sharedPreferencesMap[it.arguments[0]] } - } - // Backing store for account preferences, to allow mutation in tests accountPreferencesMap = mutableMapOf( PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA to false, diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusDisplayOptions.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusDisplayOptions.kt index 6cab00eed..f34a9b873 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusDisplayOptions.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusDisplayOptions.kt @@ -17,6 +17,7 @@ package app.pachli.components.timeline +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.settings.PrefKeys @@ -67,7 +68,9 @@ class NetworkTimelineViewModelTestStatusDisplayOptions : NetworkTimelineViewMode assertThat(defaultStatusDisplayOptions.animateAvatars).isFalse() // Given; just a change to one preferences - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When val updatedOptions = defaultStatusDisplayOptions.make( @@ -89,7 +92,9 @@ class NetworkTimelineViewModelTestStatusDisplayOptions : NetworkTimelineViewMode } // Given - sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true + sharedPreferences.edit { + putBoolean(PrefKeys.ANIMATE_GIF_AVATARS, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.ANIMATE_GIF_AVATARS)) diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestUiState.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestUiState.kt index 4722993a8..5bf72373e 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestUiState.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestUiState.kt @@ -17,6 +17,7 @@ package app.pachli.components.timeline +import androidx.core.content.edit import app.cash.turbine.test import app.pachli.appstore.PreferenceChangedEvent import app.pachli.components.timeline.viewmodel.UiState @@ -53,7 +54,9 @@ class NetworkTimelineViewModelTestUiState : NetworkTimelineViewModelTestBase() { } // Given - sharedPreferencesMap[PrefKeys.FAB_HIDE] = true + sharedPreferences.edit { + putBoolean(PrefKeys.FAB_HIDE, true) + } // When eventHub.dispatch(PreferenceChangedEvent(PrefKeys.FAB_HIDE)) diff --git a/app/src/test/java/app/pachli/fakes/InMemorySharedPreferences.kt b/app/src/test/java/app/pachli/fakes/InMemorySharedPreferences.kt new file mode 100644 index 000000000..47afb2613 --- /dev/null +++ b/app/src/test/java/app/pachli/fakes/InMemorySharedPreferences.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.fakes + +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import android.content.SharedPreferences.OnSharedPreferenceChangeListener + +/** + * An in-memory implementation of [SharedPreferences] suitable for use in tests. + * + * @param initialValues optional map of initial values + */ +@Suppress("UNCHECKED_CAST") +class InMemorySharedPreferences( + initialValues: Map? = null, +) : SharedPreferences { + private var store: MutableMap = initialValues?.toMutableMap() ?: mutableMapOf() + + private var listeners: MutableSet = HashSet() + + private val preferenceEditor: MockSharedPreferenceEditor = + MockSharedPreferenceEditor(this, store, listeners) + + override fun getAll(): Map = store + + override fun getString(key: String?, defValue: String?) = store.getOrDefault(key, defValue) as String? + + override fun getStringSet(key: String?, defValues: MutableSet?) = store.getOrDefault(key, defValues) as MutableSet? + + override fun getInt(key: String, defaultValue: Int) = store.getOrDefault(key, defaultValue) as Int + + override fun getLong(key: String, defaultValue: Long) = store.getOrDefault(key, defaultValue) as Long + + override fun getFloat(key: String, defaultValue: Float) = store.getOrDefault(key, defaultValue) as Float + + override fun getBoolean(key: String, defaultValue: Boolean) = store.getOrDefault(key, defaultValue) as Boolean + + override fun contains(key: String) = key in store + + override fun edit(): Editor = preferenceEditor + + override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + listeners.add(listener) + } + + override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + listeners.remove(listener) + } + + class MockSharedPreferenceEditor( + private val sharedPreferences: InMemorySharedPreferences, + private val store: MutableMap, + private val listeners: MutableSet, + ) : Editor { + private val edits: MutableMap = mutableMapOf() + private var deletes: MutableList = ArrayList() + + override fun putString(key: String, value: String?): Editor { + edits[key] = value + return this + } + + override fun putStringSet(key: String, values: MutableSet?): Editor { + edits[key] = values + return this + } + + override fun putInt(key: String, value: Int): Editor { + edits[key] = value + return this + } + + override fun putLong(key: String, value: Long): Editor { + edits[key] = value + return this + } + + override fun putFloat(key: String, value: Float): Editor { + edits[key] = value + return this + } + + override fun putBoolean(key: String, value: Boolean): Editor { + edits[key] = value + return this + } + + override fun remove(key: String): Editor { + edits.remove(key) + deletes.add(key) + return this + } + + override fun clear(): Editor { + deletes.clear() + store.clear() + edits.clear() + listeners.forEach { + it.onSharedPreferenceChanged(sharedPreferences, null) + } + return this + } + + override fun commit(): Boolean { + deletes.forEach { key -> + store.remove(key) + listeners.forEach { it.onSharedPreferenceChanged(sharedPreferences, key) } + } + + edits.forEach { entry -> + store[entry.key] = entry.value + listeners.forEach { it.onSharedPreferenceChanged(sharedPreferences, entry.key) } + } + + deletes.clear() + edits.clear() + return true + } + + override fun apply() { + commit() + } + } +}