How to write test cases for switch condition in android kotlin - android

I need to write test case for the switch condition in kotlin.
Class.kt
fun getEnvSwitchURL(applicationContext: Context, envSwitchInfo: String): String {
val resources = applicationContext.getResources()
val assetManager = resources.getAssets()
val properties = Properties()
try {
val inputStream = assetManager.open("configuration.properties")
properties.load(inputStream)
val urlPref = applicationContext.getSharedPreferences(SELECTED_ENV, Context.MODE_PRIVATE)
val editor = urlPref.edit()
when (envSwitchInfo) {
"Production" ->{
editor.putString("selectedUrl", properties.getProperty("prodUrl"))
editor.apply()
selectedUrl=properties.getProperty("prodUrl")
}
"Development" ->{
editor.putString("selectedUrl", properties.getProperty("devUrl"))
editor.apply()
selectedUrl=properties.getProperty("devUrl")
}
"Testing" ->{
editor.putString("selectedUrl", properties.getProperty("testUrl"))
editor.apply()
selectedUrl=properties.getProperty("testUrl")
}
}
inputStream.close()
}
return selectedUrl
}
test.kt
#BeforeEach
fun runBeforeTest() {
testApplicationContext = Mockito.mock(Context::class.java)
testResource = Mockito.mock(Resources::class.java)
testAsset = Mockito.mock(AssetManager::class.java)
testInputStream = Mockito.mock(InputStream::class.java)
testSharedPref=Mockito.mock(SharedPreferences::class.java)
testEditor=Mockito.mock(SharedPreferences.Editor::class.java)
testProperties=Mockito.mock(Properties::class.java)
testProperties.setProperty("prodUrl", "Value");
}
#Test
fun getEnvSwitchURL() {
Mockito.`when`(testApplicationContext.getResources()).thenReturn(testResource)
Mockito.`when`(testResource.assets).thenReturn(testAsset)
Mockito.`when`(testAsset.open(Mockito.anyString())).thenReturn(testInputStream)
PowerMockito.whenNew(Properties::class.java).withNoArguments().thenReturn(testProperties)
Mockito.doNothing().`when`(testProperties).load(Mockito.any(InputStream::class.java))
Mockito.`when`(testApplicationContext.getSharedPreferences(anyString(),anyInt())).thenReturn(testSharedPref)
Mockito.`when`(testSharedPref.edit()).thenReturn(testEditor)
envSwitchUtils.getEnvSwitchURL(testApplicationContext, testEnvSwitchInfo)
}
Above written test case is working fine. I need to find out how to write test case for switch condition for the above class. Kindly help me to write the same

I haven't answered your question, but perhaps refactoring your code slightly makes it more obvious to test:
private val SELECTED_ENV = "";
fun getEnvSwitchURL(applicationContext: Context, envSwitchInfo: String): String {
val resources = applicationContext.resources
val assetManager = resources.assets
val properties = Properties()
val selectedUrl: String
try {
val inputStream = assetManager.open("configuration.properties")
properties.load(inputStream)
val urlPref = applicationContext.getSharedPreferences(SELECTED_ENV, Context.MODE_PRIVATE)
val editor = urlPref.edit()
selectedUrl = get(envSwitchInfo, properties)
editor.putString("selectedUrl", selectedUrl)
editor.apply()
inputStream.close()
}
return selectedUrl
}
fun get(envSwitchInfo: String, properties: Properties): String {
when (envSwitchInfo) {
"Production" -> {
return properties.getProperty("prodUrl")
}
"Development" -> {
return properties.getProperty("devUrl")
}
"Testing" -> {
return properties.getProperty("testUrl")
}
else -> throw IllegalStateException("Unhandled environment $envSwitchInfo")
}
}
You could do a lot more here, look into the Single Responsibilty Principle. This is a start, for unit testing you don't want to test that SharePreferences works correctly because then you are testing the platform and not your code. You may want to test only that when you pass an environment like "Production", then the selectedUrl you get is returned.
Testing inputs and outputs as described above would be something like this:
String url = envSwitchUtils.getEnvSwitchURL(testApplicationContext, "Production")
assertEquals(url, "http://myProdUrl")
and another test
String url = envSwitchUtils.getEnvSwitchURL(testApplicationContext, "Development")
assertEquals(url, "http://myDevUrl")

Related

How to test ViewModel + Flow

I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.

I got different result for same input on junit tests and on Android (ART) at runtime

I have a one static method which finds matching texts on target text, but it returns different results for same input, results in Junit tests and result in Android at runtime is different.
private const val REGEX = "GE[a-zA-Z0-9]{2}\\s?([a-zA-Z0-9]{2})([0-9]{2}\\s?)([0-9]{4}\\s?){3}([0-9]{2})\\s?"
private val PATTERN: Pattern = Pattern.compile(REGEX, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
fun find(text: String): List<String> {
val textFormatted = text.replace("\\s".toRegex(), "")
val list = ArrayList<String>()
val matcher = matcher(textFormatted)
when {
matcher.matches() -> {
list.add(textFormatted)
}
matcher.find() -> {
val partitions = findGroupMatches(matcher)
list.addAll(partitions)
}
}
return list
}
private fun findGroupMatches(matcher: Matcher): List<String> {
val partitions = mutableListOf<String>()
for (i in 0 until matcher.groupCount()) {
val partition = matcher.group(i)
if (partition != null && PATTERN.matcher(partition).matches()) {
partitions.add(partition.replace("\\s".toRegex(), ""))
}
}
return partitions
}
And the magic (imho) happens here
On JVM tests, it returns emptyList
#Test
fun `find with not valid text returns emptyList`(){
val targets = PatternUtils.find("GE03TB 7433311450666666300008")
assertTrue(targets.isEmpty()) // success
}
PatternUtils.find("GE03TB 7433311450666666300008") on Android inside `onCreate()` returns 1 size list("GE03TB743331145066666") //
Why it works like that? Is there any bug? or am I missing something?
Reason was that Pattern.java implementation by Oracle and by Google are different.
In order to work similarly I had to create new Matcher instead of reusing it. (Matcher.java has some methods with side effect)
fun find(text: String): List<String> {
val textFormatted = text.replace("\\s".toRegex(), "")
val list = ArrayList<String>()
val matcher = createMatcher(textFormatted)
when {
createMatcher(textFormatted).matches() -> {
list.add(textFormatted)
}
matcher.find() -> {
val partitions = findGroupMatches(matcher)
list.addAll(partitions)
}
}
return list
}

Optimizing Android shared preference value access

I am using Androids shared preferences as a simple Storage like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private val myKey = "my_string_key"
fun getMyString(): String {
return storage.getString(myKey, "default String") ?: "default String"
}
fun setMyString(str: String) {
storage.edit().apply {
putString(myKey, str)
apply()
}
}
}
However, I don't like how this introduces boilerplate code each time I add another stored value.
This is my current workaround:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private inline fun put(block: (SharedPreferences.Editor) -> Unit) {
storage().edit().apply {
block(this)
apply()
}
}
var myString: String = "default String"
set(str) = put { it.putString(::myString.name, str) }
get() = storage.getString(::myString.name, field) ?: field
}
Now my Questions are:
Is this even a good idea? (Im new to programming on the Android platform and I haven't seen anyone do this, so there might be a good reason to stick to the function approach)
Can this be optimised further? Ideally I would like to only declare the variable and have the getter and setter generated somehow.
Thank you in advance.
I would choose different approach. You just need keys for this. For example something like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: String): String = storage.getString(key, "defaultString") ?: "defaultString"
public fun set(key: String, value: String) = storage.edit().putString(key, value)
object Keys {
const val key1 = "Key1"
const val key2 = "Key2"
}
}
Or, if you want more safety for keys to be used as constants, not strings
enum class Keys {
key1, key2
}
class Storage(context: Context) {
private fun getDefaultValue(key:Keys): String = when(key) {
Keys.key1 -> "string"
Keys.key2 -> "String2"
}
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: Keys): String =
storage.getString(key.name, null) ?: getDefaultValue(key)
public fun set(key: Keys, value: String) = storage.edit().putString(key.name, value)
}

Androidx datastore test: Ensure that you are only creating a single instance of datastore for this file

Currently, I am writing some test for my proto datastore. The only problem I have here is that I can't call a specific function because then my test fails / crashes. I find this very confusing, because all my other functions seem to work, except resetDatastore
Here is my code:
Repository
private companion object {
private const val SHOP_FILTER_PRODUCT_DATASTORE: String = "shop_filter_product_datastore_test"
private const val SHOP_FILTER_LIST_DATASTORE: String = "shop_filter_list_datastore_test"
private const val SHOP_FILTER_BTN_DATASTORE: String = "shop_filter_btn_datastore_test"
}
private val testNonVolatileProductDataStore = context.createDataStore(
fileName = SHOP_FILTER_PRODUCT_DATASTORE,
serializer = ShopFilterProductSerializer
)
private val testNonVolatileListDataStore = context.createDataStore(
fileName = SHOP_FILTER_LIST_DATASTORE,
serializer = ShopFilterListSerializer
)
private val testNonVolatileBtnDataStore = context.createDataStore(
fileName = SHOP_FILTER_BTN_DATASTORE,
serializer = ShopFilterBtnSerializer
)
override suspend fun setValueProduct(newProduct: ShopFilterTempHolder) {
if (newProduct.id == null || newProduct.mQuery == null) return
testNonVolatileProductDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = newProduct.id!!
query = newProduct.mQuery
}.build()
}
}
override suspend fun setValueList(newList: ShopFilterTempHolder) {
if (newList.id == null || newList.mQuery == null) return
testNonVolatileListDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = newList.id!!
query = newList.mQuery
mQueryDirection = newList.mQueryDirection
}.build()
}
}
override suspend fun setShopFilterBtn(value: Boolean) {
testNonVolatileBtnDataStore.updateData { preferences ->
preferences.toBuilder().apply {
isChecked = value
}.build()
}
}
override suspend fun peekProductValue(): ShopFilterTempHolder {
val temp = shopFilterProduct.first()
return ShopFilterTempHolder(temp.positionId, temp.query)
}
override suspend fun peekListValue(): ShopFilterTempHolder {
val temp = shopFilterList.first()
return ShopFilterTempHolder(temp.positionId, temp.query, temp.mQueryDirection)
}
override suspend fun peekBtnValue(): Boolean = mappedShopFilterBtn.first()
override suspend fun resetDatastore() {
testNonVolatileProductDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = Constants.SHOP_FILTER_DEFAULT_PRODUCT_ID
query = Constants.SHOP_FILTER_DEFAULT_PRODUCT_QUERY
}.build()
}
testNonVolatileListDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = Constants.SHOP_FILTER_DEFAULT_LIST_ID
query = Constants.SHOP_FILTER_DEFAULT_LIST_QUERY
mQueryDirection = Constants.SHOP_FILTER_DEFAULT_LIST_QUERY_DIRECTION
}.build()
}
testNonVolatileBtnDataStore.updateData { preferences ->
preferences.toBuilder().apply {
isChecked = true
}.build()
}
}
Test
#Test
fun `values should be set to default`() = runBlocking {
val newBtn = false
val newList = ShopFilterTempHolder(0, "testString", 0)
val newProduct = ShopFilterTempHolder(0, "testString", 0)
shopFilterValidator.tempBtnFilterValue = newBtn
shopFilterValidator.tempListFilter = newList
shopFilterValidator.tempProductFilter = newProduct
shopFilterValidator.setNewBtnFilter()
shopFilterValidator.setNewListFilter()
shopFilterValidator.setNewProductFilter()
assertEquals(newProduct, shopFilterDataStoreRepository.peekProductValue())
assertEquals(newList, shopFilterDataStoreRepository.peekListValue())
assertEquals(newBtn, shopFilterDataStoreRepository.peekBtnValue())
shopFilterValidator.deleteAllValues()
assertEquals(defautTempProductFilter, shopFilterDataStoreRepository.peekProductValue())
assertEquals(defaultTempListFilter, shopFilterDataStoreRepository.peekListValue())
assertEquals(defaultTempBtnFilterValue, shopFilterDataStoreRepository.peekBtnValue())
}
Stacktrace
Exception in thread "DefaultDispatcher-worker-2 #coroutine#5" java.io.IOException: Unable to rename C:\Users\Censored\AppData\Local\Temp\robolectric-Method_values_should_be_set_to_default1366629743868428403\com.example.app-dataDir\files\datastore\shop_filter_product_datastore_test.tmp.This likely means that there are multiple instances of DataStore for this file. Ensure that you are only creating a single instance of datastore for this file.
at androidx.datastore.core.SingleProcessDataStore.writeData$datastore_core(SingleProcessDataStore.kt:303)
at androidx.datastore.core.SingleProcessDataStore.transformAndWrite(SingleProcessDataStore.kt:280)
at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:165)
(Coroutine boundary)
at kotlinx.coroutines.CompletableDeferredImpl.await(CompletableDeferred.kt:86)
at androidx.datastore.core.SingleProcessDataStore$updateData$2.invokeSuspend(SingleProcessDataStore.kt:96)
at androidx.datastore.core.SingleProcessDataStore.updateData(SingleProcessDataStore.kt:96)
at com.example.app.repository.FakeDataStoreRepositoryImpl.deleteDataStore(FakeDataStoreRepositoryImpl.kt:86)
at com.example.app.data.models.validator.ShopFilterValidator$deleteAllValues$1.invokeSuspend(ShopFilterValidator.kt:80)
not sure if that could help you, but in my case the problem occurred when running tests on Windows machine and wasn't there when switching to Linux or executing the test on the emulator instead

Android (Kotlin) - How do I wait for an asynchronous task to finish?

I am new to Android and Kotlin and am currently working on a centralized API router class.
To achieve this I am using the Fuel Framework.
For the doAsync function, I use the Anko for Kotlin library.
To retrieve an authorization token from the API I currently use this method:
private fun Login(username: String, password: String, callback: (Map<Boolean, String>) -> Unit) {
"/auth/token.json".httpPost()
.header(mapOf("Content-Type" to "application/json"))
.body("""{"username":"$username", "password":"$password"}""", Charsets.UTF_8)
.response { request, response, result ->
request.headers.remove("Accept-Encoding")
when (result) {
is Result.Failure -> {
// val data = result.get()
val ex = result.getException()
val serverResponseJson = response.data.toString(Charsets.UTF_8)
var exceptionMessage = ex.message
val jelement = JsonParser().parse(serverResponseJson)
val jobject = jelement.asJsonObject
val serverResponseError = if (jobject.has("Error")) jobject.get("Error").asString else jobject.get("detail").asString
callback(mapOf(Pair(false, serverResponseError)))
}
is Result.Success -> {
val data = result.get()
val returnJson = data.toString(Charsets.UTF_8)
Log.println(Log.ASSERT, "RESULT_LOGIN", returnJson)
callback(mapOf(Pair(true, returnJson)))
}
}
}
}
I invoke this login method at
val btnLogin = findViewById<Button>(R.id.btn_login)
btnLogin.setOnClickListener { _ ->
doAsync {
val username = findViewById<EditText>(R.id.input_username_login)
val password = findViewById<EditText>(R.id.input_password_login)
Login(username.text.toString(), password.text.toString()) {
// Request was successful
if (it.containsKey(true)) {
// Parse return Json
// e.g. {"id":"36e8fac0-487a-11e8-ad4e-c471feb11e42","token":"d6897a230fd7739e601649bf5fd89ea4b93317f6","expiry":"2018-04-27T17:49:48.721278Z"}
val jelement = JsonParser().parse(it.getValue(true))
val jobject = jelement.asJsonObject
// save field for class-scope access
Constants.token = jobject.get("token").asString
Constants.id = jobject.get("id").asString
}
else{
Toast.makeText(this#LoginActivity, it.getValue(false), Toast.LENGTH_SHORT).show()
}
}
}[30, TimeUnit.SECONDS]
var test = Constants.id;
}
In a separate Constants class, I store the token and id like this:
class Constants {
companion object {
val baseUrl: String = "BASE_URL_TO_MY_API"
val contentTypeJson = "application/json"
lateinit var STOREAGE_PATH: String
// current user details
lateinit var id: String
lateinit var token: String
lateinit var refresh_token: String
// logged in User
lateinit var user: User
}
How do I make sure that the test variable is set after the asynchronous task is done? Currently, I run into
lateinit property id has not been initialized
I have come across the option to limit the task to a timeout such as I have done with [30, TimeUnit.SECONDS], unfortunately, this did not help.
Thanks for the help! Cheers.
I think the problem is where you want to access the result:
val btnLogin = findViewById<Button>(R.id.btn_login)
btnLogin.setOnClickListener { _ ->
doAsync {
val username = findViewById<EditText>(R.id.input_username_login)
val password = findViewById<EditText>(R.id.input_password_login)
var test: String? = null
Login(username.text.toString(), password.text.toString()) {
// Request was successful
if (it.containsKey(true)) {
// Parse return Json
// e.g. {"id":"36e8fac0-487a-11e8-ad4e-c471feb11e42","token":"d6897a230fd7739e601649bf5fd89ea4b93317f6","expiry":"2018-04-27T17:49:48.721278Z"}
val jelement = JsonParser().parse(it.getValue(true))
val jobject = jelement.asJsonObject
// save field for class-scope access
Constants.token = jobject.get("token").asString
Constants.id = jobject.get("id").asString
}
else{
Toast.makeText(this#LoginActivity, it.getValue(false), Toast.LENGTH_SHORT).show()
}
}
test = Constants.id // here test variable surely set if result was successful, otherwise it holds the null value
test?.let{
resultDelivered(it)
}
}[30, TimeUnit.SECONDS]
}
fun resultDelivered(id: String){
// here we know that the async job has successfully finished
}

Categories

Resources