I have this Kotlin class that is a wrapper around SharedPreferences in an Android app.
class Preferences(private val context: Context) {
private val preferences: SharedPreferences =
context.getSharedPreferences("name_of_file", Context.MODE_PRIVATE)
// Integers
var coins: Int
get() = preferences.getInt(KEY_COINS, 0)
set(value) = preferences.edit { putInt(KEY_COINS, value) }
var pressure: Int
get() = preferences.getInt(KEY_PRESSURE, DEFAULT_PRESSURE)
set(value) = preferences.edit { putInt(KEY_PRESSURE, value) }
}
I need to mock this class in order to be able to use it in some unit tests for my viewmodels. I tried mocking the get/set methods of the properties but for some reason I'm getting some errors and I need a bit of help.
This is how I try to mock the Preferences class:
private val sharedPreferences = mutableMapOf<String, Any>()
...
val preferences = mockk<Preferences>()
listOf("coins", "pressure").forEach { key ->
every { preferences getProperty key } returns sharedPreferences[key]
every { preferences setProperty key } answers { // exception on this line
sharedPreferences[key] = fieldValue
fieldValue
}
}
And I get this exception when running any of the tests that involves this mock
io.mockk.MockKException: Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
I think the error is quite cryptic. Or, is there any way to mock these fields using mockk?
I have also read the examples from here where I got the inspiration for this solution but seems like there is something I'm missing.
Related
I want to reference an object within this class I have below:
class HerbData {
object Dill {
const val herbName: String = "This is Dill!"
const val scientificName: String = "Anethum Graveolens"
val dullThumbnail: Int = R.drawable.dill_thumbnail_attr
}
object Peppermint {
val herbName: String = "This is Peppermint!"
}
}
Is there anyway that I can reference the object by using a string in Kotlin? Here is somewhat what I mean:
HerbData."Dill".herbname
I can't find anything on this topic for Kotlin.
Another way you could do this is with an enum class. The advantage over a map is that you have a data structure you can reference directly in code, so you could use HerbData.Dill as well as HerbData["Dill"]. And that will enable you to take advantage of compile-time checking and lint warnings, refactoring, exhaustive pattern matching, code completion etc, because the data is defined in your code
enum class HerbData(
val herbName: String,
val scientificName: String? = null,
val dullThumbnail: Int? = null
) {
Dill("This is Dill!", "Anethum Graveolens", R.drawable.dill_thumbnail_attr),
Peppermint("This is Peppermint!");
companion object {
operator fun get(name: String): HerbData? =
try { valueOf(name) } catch(e: IllegalArgumentException) { null }
}
}
fun main() {
// no guarantee these lookups exist, need to null-check them
HerbData["Peppermint"]?.herbName.run(::println)
// case-sensitive so this fails
HerbData["peppermint"]?.herbName.run(::println)
// this name is defined in the type system though! No checking required
HerbData.Peppermint.herbName.run(::println)
}
>> This is Peppermint!
null
This is Peppermint!
Enum classes have that valueOf(String) method that lets you look up a constant by name, but it throws an exception if nothing matches. I added it as a get operator function on the class, so you can use the typical getter access like a map (e.g. HerbData["Dill"]). As an alternative, you could do something a bit neater:
companion object {
// storing all the enum constants for lookups
private val values = values()
operator fun get(name: String): HerbData? =
values.find() { it.name.equals(name, ignoreCase = true) }
}
You could tweak the efficiency on this (I'm just storing the result of values() since that call creates a new array each time) but it's pretty simple - you're just storing all the enum entries and creating a lookup based on the name. That lets you be a little smarter if you need to, like making the lookup case-insensitive (which may or may not be a good thing, depending on why you're doing this)
The advantage here is that you're generating the lookup automatically - if you ever refactor the name of an enum constant, the string label will always match it (which you can get from the enum constant itself using its name property). Any "Dill" strings in your code will stay as "Dill" of course - that's the limitation of using hardcoded string lookups
The question really is, why do you want to do this? If it's pure data where no items need to be explicitly referenced in code, and it's all looked up at runtime, you should probably use a data class and a map, or something along those lines. If you do need to reference them as objects within the code at compile time (and trying to use HerbData."Dill".herbName implies you do) then an enum is a fairly easy way to let you do both
Declare a Data Class
data class HerbData (
val scientificName: String,
val dullThumbnail: Int
)
Initialize a muteable map and put data in it
val herbData = mutableMapOf<String, HerbData>()
herbData.put("Dill", HerbData("Anethum Graveolens", R.drawable.dill_thumbnail_attr))
herbData.put("Peppermint", HerbData("Mentha piperita", R.drawable.peppermint_thumbnail_attr))
You can now just
herbData["Dill"]?.scientificName
class HerbData {
interface Herb {
val herbName: String
val scientificName: String
}
object Dill : Herb {
override val herbName: String = "This is Dill!"
override val scientificName: String = "Anethum Graveolens"
}
object Peppermint: Herb {
override val herbName: String = "This is Peppermint!"
override val scientificName: String = "Mentha piperita"
}
companion object {
operator fun get(name: String): Herb? {
return HerbData::class
.nestedClasses
.find { it.simpleName == name }
?.objectInstance as? Herb
}
}
}
println(HerbData["Dill"]?.herbName) // Prints: This is Dill!
println(HerbData["Peppermint"]?.scientificName) // Prints: Mentha piperita
println(HerbData["Pepper"]?.herbName) // Prints: null
I've been using Datastore for a long time. Today i had to read the values in the main thread. After reviewing the documentation, I decided to use runblocking. I created a long value which name is lastInsertedId.
I reading lastInsertedId in Fragment A then navigated to Fragment B and I'm changing the value of lastInsertedId. When i pop back to Fragment A i read lastInsertedId again. But lastInsertedId's value was still same. Actually it's value is changing but i can't read its last value.
I think it was because Fragment A was not destroyed. Only onDestroyView called and created from onCreateView. What i want is i need to access lastInsertedID's current value whenever i want in main thread.
When i create it as a variable, it always returns the same value. But when i convert it to function it works well. But i don't think this is the best practices. What's the best way to access this value?
Thanks.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "main")
#Singleton
class DataStoreManager #Inject constructor(#ApplicationContext appContext: Context) {
private val mainDataStore = appContext.dataStore
suspend fun setLastInsertedId(lastId: Long) {
mainDataStore.edit { main ->
main[LAST_INSERTED_ID] = lastId
}
}
// Returns always the same value
val lastInsertedId: Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// Returns as expected
fun lastInsertedId(): Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// This is also work perfectly but i need to access in main thread.
val lastInsertedId : Flow<Long> = mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: Constants.DEFAULT_FOOD_ID
}
companion object {
private val LAST_INSERTED_ID = longPreferencesKey("last_inserted_id")
}
}
You must add get() to your val definition like this.
val lastInsertedId: Long get() = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
You seems don't understand the differece between variable and function, take a look at this:
fun main() {
val randomNum1 = (1..10).random()
repeat(5) { println(randomNum1) }
repeat(5) { println(getRandomNum()) }
}
fun getRandomNum() = (1..10).random()
Output:
2
2
2
2
2
8
7
8
10
1
Variable holds a value, and it doesn't change until you assign it a new value.
SharedPreferences provide the following function to retrieve a string:
String getString(String key, #Nullable String defValue);
When storing an Int, the default-value is not-nullable:
int getInt(String key, int defValue);
I now am looking to store and retrieve a nullable Int?, e.g.:
var timerDuration: Int?
get() = prefs.getIntOrNull(TIMER_DURATION)
set(value) { prefs.edit { putIntOrNull(TIMER_DURATION, value) } }
Solution 1: when value is null, remove key
private fun SharedPreferences.getIntOrNull(key: String) =
if (prefs.contains(key)) {
getInt(key, 12345)
} else {
null
}
private fun SharedPreferences.Editor.putIntOrNull(key: String, value: Int?) =
if (value == null) {
remove(key)
} else {
putInt(key, value)
}
... but i'm not sure if i could get problems with multiple, quick accesses because of the asynchronous nature of shared preferences?
Solution 2: use the string-option for everything:
private fun SharedPreferences.getIntOrNull(key: String) =
prefs.getString(key, null)?.toIntOrNull()
private fun SharedPreferences.Editor.putIntOrNull(key: String, value: Int?) =
putString(key, value?.toString())
... but .toIntOrNull() feels like a lot of overhead for such a simple task?
Why would i like to do this?
I hope Kotlin Multiplattform allows me to add an iOS-Version to my existing Android-App.
My goal is a "core" module written completely in Kotlin with no plattform-specific dependencies.
The "core" then just uses this interface, which is implemented by the Android & iOS - Apps
interface SettingsStorage {
var timerDuration: Int?
[...]
}
And as i would not like to have duplicate default-value-logic i'd enjoy to handle that in my "core"-module
Are there any other (better) options? I'm feeling like i'm reinventing the wheel...
Either of your methods is fine. SharedPreferences is thread-safe. Option 1 could only really fail if you set up a service in your manifest to run in a separate process and it is modifying the preferences at the same time as the rest of your app. This isn't something you would do accidentally.
Your second method isn't any additional overhead, because under the hood, SharedPreferences are all Strings so your code is basically doing the same thing as SharedPreferences.getInt, except that returns the default value when the preference doesn't exist or cant be parsed as an Int.
When you store an Int to SharedPreferences, it is stored internally as a string. So for getting, just get a String and if it's a null returns it, or try parse it to an Int. For setting, just convert your Int to String and put it using putString method.
var timerDuration: Int?
get() = try { prefs.getString(TIMER_DURATION, null)?.toInt() } catch (e: Exception) { null }
set(value) { prefs.edit { putString(TIMER_DURATION, value.toString()) } }
I switched from SharedPreferences to Jetpack DataStore.
I am unable to mock the data from the datastore call in the Instrumentation test
In declaration
val dataStore = context.createDataStore(name = "App Name")
In the declaration get a string from preferences
suspend fun getString(
key: Preferences.Key<String>,
defaultVal: String,
context: Context
): String {
return dataStore?.data?.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}?.map {
it[key] ?: defaultVal
}?.first() ?: ""
}
In the usage
SharedPreferenceHelper.getString(
Preferences.Key<T>,
"",
requireContext()
)
Manipulate datastore preferences string key to get mocked value in instrumentation test as the desired value.
Thank you in advance.
createDataStore returns DataStore<Preference>, this will help you to create mock of Class with generic type
Or you can use mockito-kotlin to create mock like this in kotlin
val mockDataStore = mock<DataStore<Preference>>()
I am coming into a legacy codebase which uses SharedPreferences for data persistence. I'd like to test saving/retrieving a value, mocking using MockK. However, the assertion in this unit test never passes. It is as if the SharedPrefs aren't stored properly in testing:
class MyProfilePrefsTest {
private lateinit var myProfilePrefs: ProfilePrefs
#RelaxedMockK private lateinit var mockSharedPrefs: SharedPreferences
#RelaxedMockK private lateinit var context: Context
#Before
fun setup() {
MockKAnnotations.init(this)
val sharedPreferences = mockk<SharedPreferences>()
every { sharedPreferences.edit() } returns (mockk())
myProfilePrefs = ProfilePrefs(context, sharedPreferences)
mockStatic(DeviceInfo::class)
every { DeviceInfo.serialNumber() } returns "fake_serial"
}
#Test
fun `Saving correct cellular download pref for device id`() {
// Arrange
val isEnabled = true
// Act
myProfilePrefs.setCellularDownloadingEnabled(isEnabled)
// Assert
assertTrue(myProfilePrefs.getCellularDownloadingEnabled())
}}
Anybody know how to unit test SharedPrefs?
You need Robolectric library to test classes related to the Context. This library will simulate an Android device(without emulator).
In that case, you can use RuntimeEnvironment.application.getApplicationContext() which will return real, not mocked object of Context class.
update as of May 2020:
RuntimeEnvironment.application.getApplicationContext() is now Deprecated.
please use ApplicationProvider.getApplicationContext() to get the Context. also, keep in mind that you should add testImplementation 'androidx.test:core:1.2.0' to your build.gradle.
so
Espresso can help you as well, but it is instrumental tests though.
Thanks to #samaromku's suggested answer above. Here is the full solution. It uses AndroidX Test runner:
#RunWith(AndroidJUnit4::class)
class ProfilePrefsTest {
private lateinit var profilePrefs: ProfilePrefs
private lateinit var context: Context
#Before
fun setup() {
context = getApplicationContext<MyApplication>()
val sharedPreferences = context.getSharedPreferences(
"prefs",
MODE_PRIVATE
);
profilePrefs = ProfilePrefs(context, sharedPreferences)
mockStatic(DeviceInfo::class)
every { DeviceInfo.serialNumber() } returns FAKE_SERIAL_NUMBER
}
#Test
fun `Saving correct cellular download pref for device id`() {
// Arrange
val isEnabled = true
// Act
profilePrefs.setCellularDownloadingEnabled(isEnabled)
// Assert
assertTrue(profilePrefs.isCellularDownloadingEnabled())
}
}