I am testing with MockWebServer.
And I need a lot of json files for request and response data.
Hard coded json values seem messy and I want to create json files instead.
So, I created json files in resources(test). And I tried to read file with these methods.
object TestHelper {
fun read(fileName: String): String {
val resource = javaClass.classLoader?.getResource(fileName)
return resource?.readText() ?: ""
}
fun readJson(fileName: String): String {
val byteArray = readBinaryFileFromResources(fileName)
val sb = StringBuilder("")
byteArray.forEach {
println("byte: $it")
sb.append(it as Char)
}
return sb.toString()
}
#Throws(IOException::class)
fun readBinaryFileFromResources(fileName: String): ByteArray {
var inputStream: InputStream? = null
val byteStream = ByteArrayOutputStream()
try {
inputStream = javaClass.classLoader?.getResourceAsStream(fileName)
var nextValue = inputStream?.read() ?: -1
while (nextValue != -1) {
byteStream.write(nextValue)
nextValue = inputStream?.read() ?: -1
}
return byteStream.toByteArray()
} catch (e: Exception) {
println(e.stackTraceToString())
return byteStream.toByteArray()
} finally {
inputStream?.close()
byteStream.close()
}
}
}
None of them seems work. What's the problem with this code?
I've had trouble with this before, and I believe it has to do with getting the correct classLoader from the call site, as well as having resources in the src/test/resources not being accessible properly. I eventually got it to work by passing in the calling test class as a reified type parameter:
inline fun <reified T> loadFileText(
caller: T,
filePath: String
): String =
T::class.java.getResource(filePath)?.readText() ?: throw IllegalArgumentException(
"Could not find file $filePath. Make sure to put it in the correct resources folder for $caller's runtime."
)
For my setup I have a separate shared module :testtools that I use testImplementation to include in my :app's gradle build so they don't get compiled into the production APK. I have my test resources in:
/testtools/src/main/resources/customfolder
And then calling this from a unit test class in :app like so:
class UnitTestClass {
#Test
fun myTest() {
loadFileText(this, "/customfolder/file_name.txt")
}
}
You might have some luck putting your resources straight into /app/src/test/resources/customfolder, I haven't tried in a while.
Related
I need to test three things from the below mentioned method:
verify output.write(any<ByteArray>()) is called
verify output.close() is called
assert that fullPath is returned
fun saveFile(fullPath: String, model: SomeDataModel): String? {
try {
val output = FileOutputStream(fullPath)
output.write(Base64.decode(model.someString, Base64.DEFAULT))
output.close()
} catch (e: IOException) {
return null
}
return fullPath
}
Facing issue:
Mocking FileOutputStream
Tried:
#Test
fun `saveFile returns file path if fileOutputStream write succeeds`() {
val fullPath = "test/full/path"
val model = SomeDataModel()
val stringByteArray: ByteArray? = someModel.someString?.toByteArray(Charset.defaultCharset())
mockkStatic("android.util.Base64")
every { Base64.decode(model.someString, Base64.DEFAULT) } returns stringByteArray
mockkConstructor(FileOutputStream::class)
val fileOutputStream = mockk<FileOutputStream>()
every { constructedWith<FileOutputStream>(OfTypeMatcher<String>(String::class)) } returns fileOutputStream
// Getting error on above line: Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
every { saveFile(fullPath, model) } returns filePath
val result = saveFile(fullPath, model)
verify { fileOutputStream.write(stringByteArray) }
assertEquals(fullPath, result)
}
Please help me to write correct test case using Mockk.io/Mockito
In line every { constructedWith<FileOutputStream>(OfTypeMatcher<String>(String::class)) } returns fileOutputStream you must not return the mock object fileOutputStream, but a non-mock object. Have a look at https://mockk.io/#constructor-mocks
Extension functions that returns save Bitmap to device storage and returns result using sealed and class that represent its state.
sealed class RequestStatus<T> {
data class Loading<T>(val data: T? = null) : RequestStatus<T>()
data class Success<T>(val data: T) : RequestStatus<T>()
data class Failed<T>(val error: Exception, val data: T? = null) : RequestStatus<T>() {
val message = nullableErrorMsg(error.localizedMessage)
}
}
fun View.createAndStoreScreenshot(
appName: String,
#ColorRes backgroundColor: Int
): Flow<RequestStatus<Uri>> = flow {
emit(RequestStatus.Loading())
... Some code
try {
FileOutputStream(imageFile).use { stream ->
// Create bitmap screen capture
val bitmap = Bitmap.createBitmap(createBitmapFromView(backgroundColor))
// This method may take several seconds to complete, so it should only be called from a worker thread.
// Read https://developer.android.com/reference/android/graphics/Bitmap#compress(android.graphics.Bitmap.CompressFormat,%20int,%20java.io.OutputStream)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
val uriForFile = FileProvider.getUriForFile(
context.applicationContext,
"${context.applicationContext.packageName}.provider",
imageFile
)
emit(RequestStatus.Success(uriForFile))
}
}
catch (e: Exception) {
emit(RequestStatus.Failed(e))
}
}
Usage
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
view.createAndStoreScreenshot(
getString(R.string.app_name),
R.color.colorWhite_PrimaryDark
).onEach { req ->
when (req) {
is RequestStatus.Loading -> {
...
}
}.collect()
}
}
FileOutputStream is complaining as it can block UI thread, but you cannot switch context inside flow builder so we move on using flowOn(Dispatchers.IO) and remove the withContext(Dispatchers.IO) from the caller. Now we the Android Studio is complaining Not enough information to infer type variable T when emitting Loading and Failed state but explicitly providing the data type also marked as redundant. What is the problem here?
It should be Uri? not Uri for Loading and Failed state.
How to read data from JSON file on the resources directory?
I need to read a json file on the resources directory, convert it to a data class ("User")
I'm trying to adapt the following code
private fun getJSONFromAssets(): String? {
var json: String? = null
val charset: Charset = Charsets.UTF_8
try {
val myUsersJsonFile = assets.open("users.json")
val size = myUsersJsonFile.available()
val buffer = ByteArray(size)
myUsersJsonFile.read(buffer)
myUsersJsonFile.close()
json = String(buffer, charset)
} catch (ex: IOException) {
ex.printStackTrace()
return null
}
return json
}
but assets.open("users.json") is not recognized.
How is the best approach to read JSON files on the resources directory (mock data)?
You just need a minor change in your function...
private fun getJSONFromAssets(context: Context): String? {
...
val myUsersJsonFile = context.assets.open("users.json")
...
}
Assuming that your json file is at src/main/assets.
If you need to read a JSON file from the src/main/res/raw folder. You can use:
private fun getJSONFromAssets(context: Context): String? {
...
val myUsersJsonFile = context.resources.openRawResource(R.raw.users)
...
}
As you can see, you need a Context, so you can call from your activity.
getJSONFromAssets(this) // "this" is your activity (or another Context)
Background: GSON, Kotlin, Retrofit
I am writing a restaurant app. In the home page, the user is able to load a list of restaurant brands. Each brand can have up to 3 cuisine types, the first one is non-null and the next two are nullable. Each cuisine type is within the CuisineType enum class.
What I would like to do is to create a joined string like this:
cuisineType1.title + cuisineType2?.title + cuisineType3?.title = combinedCuisines. This can make all cuisines shown within a textView in Chinese. In order to do this I created a helper class. In this helper class, if the CuisineType from Brand cannot map any of the enum members, it will display the raw name from the Brand JSON(incase of server error). I tried the three solutions commented out below and non of them work. Much help would be appreciated. Thanks in advance!
data class Brand(
#SerializedName("id")
val id: Int,
#SerializedName("name_en")
val nameEN: String?,
#SerializedName("cuisine_1")
val cuisineType1: String,
#SerializedName("cuisine_2")
val cuisineType2: String?,
#SerializedName("cuisine_3")
val cuisineType3: String?,
/*Solution 1(not working):
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***java.lang.IllegalArgumentException: Unable to create converter for class
*/
/*Solution 2(not working):
#Transient
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***combinedCuisines = null after network call in fragment
*/
) {
/* Solution 3(not working):
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***problem with GSON, I can only map the #SerializedName from the Cuisine enum class and will only run the illegal argument solution from the CombineCuisineHelper. For example, get hong_kong_style from the JSON brand but it will not convert to HongKongStyle and map to its title.
*/
}
//It should be a long list but I shortened it.
enum class CuisineType {
#SerializedName("chinese")
Chinese,
#SerializedName("hong_kong_style")
HongKongStyle,
#SerializedName("cantonese")
Cantonese,
val title: Double
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
class CombineCuisineHelper {
companion object {
fun combineCuisines(cuisineSubtype1: String, cuisineSubtype2: String?, cuisineSubtype3: String?): String {
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
if (cuisineSubtype2 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype2).title
} catch (e: IllegalArgumentException) {
cuisineSubtype2
}
}
if (cuisineSubtype3 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype3).title
} catch (e: IllegalArgumentException) {
cuisineSubtype3
}
}
}
The first and second solutions are not good, because the data might not be ready at the initialization time. The third solution is the one we can continue on:
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
The SerializedNames are useless for enum constants and won't work for you as you are expecting. So the valueOf method for enum won't find a value for literals like "hong_kong_style" and will throw an exception.
You can create your own helper method in your enum class like this:
enum class CuisineType {
Chinese,
HongKongStyle,
Cantonese;
val title: String
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
}
companion object {
//Note this helper method which manually maps string values to enum constants:
fun enumValue(title: String): CuisineType = when (title) {
"chinese" -> Chinese
"hong_kong_style" -> HongKongStyle
"cantonese" -> Cantonese
else -> throw IllegalArgumentException("Unknown cuisine type $title")
}
}
}
And then use this new method instead of the enum's own valueOf method:
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineType.enumValue(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
//continued...
I have an array of URLs, each providing a zip file. I want to download them and store them in my app folders, inside the internal memory.
Question:
Since I do not know the number of URLs I will need to access, what is the best way to go about this? I am just beginning to work with Kotlin coroutines.
This is my 'download from url' method
fun downloadResourceArchiveFromUrl(urlString: String, context: Context): Boolean {
Timber.d("-> Started downloading resource archive.. $urlString")
lateinit var file: File
try {
val url = URL(urlString)
val urlConn = url.openConnection()
urlConn.readTimeout = 5000
urlConn.connectTimeout = 10000
val inputStream = urlConn.getInputStream()
val buffInStream = BufferedInputStream(inputStream, 1024 * 5)
val fileNameFromUrl = urlString.substringAfterLast("/")
file = File(context.getDir("resources", Context.MODE_PRIVATE) , fileNameFromUrl)
val outStream = FileOutputStream(file)
val buff = ByteArray(5 * 1024)
while (buffInStream.read(buff) != -1){
outStream.write(buff, 0, buffInStream.read(buff))
}
outStream.flush()
outStream.close()
buffInStream.close()
} catch (e: Exception) {
e.printStackTrace()
Timber.d("Download finished with exception: ${e.message} -<")
return false
}
Timber.d("Download finished -<")
return true
}
Could you simply create a loop and call download method each time?
for (i in resources.indices) {
asyncAwait {
downloadResourcesFromUrl(resources[i].url, context)
return#asyncAwait
}
Also, is it a good idea to do this synchronously? Wait for every file to download then proceed to the next one?
Turn your blocking download function into a suspending one:
suspend fun downloadResourceArchiveFromUrl(
urlString: String, context: Context
): Boolean = withContext(Dispatchers.IO) {
... your function body
}
Now run your loop inside a coroutine you launch:
myActivity.launch {
resources.forEach {
val success = downloadResourceArchiveFromUrl(it.url, context)
... react to success/failure ...
}
}
Also be sure to properly implement structured concurrency on your activity.