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.
This commit is contained in:
Nik Clayton 2023-10-12 11:22:41 +02:00 committed by GitHub
parent 628b5a7db5
commit 0902b0ba49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 212 additions and 62 deletions

View File

@ -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<String, Boolean>
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,

View File

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

View File

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

View File

@ -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<String, Boolean>
protected lateinit var sharedPreferences: SharedPreferences
protected lateinit var accountPreferencesMap: MutableMap<String, Boolean>
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,

View File

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

View File

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

View File

@ -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<String, Boolean>
protected lateinit var sharedPreferences: SharedPreferences
protected lateinit var accountPreferencesMap: MutableMap<String, Boolean>
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,

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses>.
*/
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<String, Any?>? = null,
) : SharedPreferences {
private var store: MutableMap<String, Any?> = initialValues?.toMutableMap() ?: mutableMapOf()
private var listeners: MutableSet<OnSharedPreferenceChangeListener> = HashSet()
private val preferenceEditor: MockSharedPreferenceEditor =
MockSharedPreferenceEditor(this, store, listeners)
override fun getAll(): Map<String, Any?> = store
override fun getString(key: String?, defValue: String?) = store.getOrDefault(key, defValue) as String?
override fun getStringSet(key: String?, defValues: MutableSet<String>?) = store.getOrDefault(key, defValues) as MutableSet<String>?
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<String, Any?>,
private val listeners: MutableSet<OnSharedPreferenceChangeListener>,
) : Editor {
private val edits: MutableMap<String, Any?> = mutableMapOf()
private var deletes: MutableList<String> = ArrayList()
override fun putString(key: String, value: String?): Editor {
edits[key] = value
return this
}
override fun putStringSet(key: String, values: MutableSet<String>?): 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()
}
}
}