#Composable
fun MyComposable(submit: (String) -> Unit) {
MyButton(title = "Submit") {
submit("Hello World!")
}
}
#Composable
fun MyButton(title: String, submit: (String) -> Unit) {
Button(onClick = submit) {
Text(text = title)
}
}
How can I unit test that submit were to be called?
While in the test I can set the submit result inside MyComposable, it never actually gets called when the test is being ran.
The test would look like this:
#Test
fun trySubmit_expectPrint() {
composeRule.setContent { GetDefaultScreen() }
//Too lazy to write the hasTestTag, but assume it gets the correct node.
composeRule.onNodeWithTag("MyComposable").performClick()
//Assertions here...
}
#Composable
private fun GetDefaultScreen() =
MyComposable() {
Log.d("TEST", "Submitting")
}
My end goal here is to have the scope of that composable in the test change a variable, in order to allow me to verify that that block was ran.
This is all under the assumption that we are still unable to Mock or Spy on final classes with an androidTest (UI test).
If that assumption is wrong, please correct me if there's a method in which I can do that.
I've since come to found something that works for me.
In the test function, just adding a value that should get changed by the callback, and then making an assertion on that value:
#Test
fun myTest() {
var testValue = ""
composeTestRule.setContent {
Button() { testValue = "Hello, World!" }
}
assertThat(testValue).equals("Hello, World!")
}
Related
I have the following code, in the click event I need to use the class that is queried from the database.
data class Work(...)
fun Compose(){
var work1: Work? = null
var work2: Work? = null
var work3: Work? = null
LaunchedEffect(true){
CoroutineScope(IO).launch {
work1 = workViewModel.getById(1)
work2 = workViewModel.getById(2)
work3 = workViewModel.getById(3)
}
}
Card(
modifier = Modifier
.clickable(onClick = {
val url = "https://www.google.com/"
when{
url.contains(work1?.baseUrl) -> {...}
url.contains(work2?.baseUrl) -> {...}
url.contains(work3?.baseUrl) -> {...}
}
})
){}
}
this creates a problem, work3?.baseUrl found String? type Required CharSequence type.
So far it seems that only the !! operator can successfully run this code. But this code is based on a database query, using the !! operator is very risky.
And if you add a null operator before this, also not working.
requireNotNull(work1)
when{
url.contains(work1.baseUrl) -> {...}
}
Smart cast to 'Work' is impossible, because 'work1' is a local variable that is captured by a changing closure
Can you tell me what is the best solution?
I suggest not having that logic in the Composable. Try to move that to a function in the ViewModel, something like:
private val url = "https://www.google.com/"
fun validateBaseUrl() {
viewModelScope.launch(Dispatchers.IO) {
workViewModel.getById(1)?.let {
if (url.contains(it)) { .. }
}
work2 = workViewModel.getById(2)?.let {
if (url.contains(it)) { .. }
}
work3 = workViewModel.getById(3)?.let {
if (url.contains(it)) { .. }
}
}
}
And then in the Composable would be something like:
fun Compose() {
Card(
modifier = Modifier
.clickable(onClick = { viewModel.validateBaseUrl() })
){}
}
Remember to use the State Hoisting instead of sending the view model through the Composables.
Finally, you would need to send back a State to the Composable, either using a LiveData or a StateFlow.
You need to create a scope for the work properties :
work1?.run { url.contains(baseUrl) } == true -> {...}
Within the run lambda the accessed object is immutable even if the work properties themselves are mutable.
The == true is needed because the left side of the comparison operator can be either a Boolean or null.
You could also define an extension function like this:
fun Work?.isContainedIn(url: String) = this?.run { url.contains(baseUrl) } == true
and then just do:
work1.isContainedIn(url) -> { }
work2.isContainedIn(url) -> { }
work3.isContainedIn(url) -> { }
I have a ViewModel that uses StateFlow.asLiveData() to expose a Repository class's StateFlow items and I'm trying to write a test for the ViewModel. My tests are configured with a Mock of an Observer<LoadingStatus> on the ViewModel's exposed LiveData.
The code I'm testing calls this method to update its loading status:
suspend fun MutableStateFlow<LoadingStatus>.performWithStatusUpdates(operation: suspend () -> Unit) {
this.value = LoadingStatus.Loading()
try {
operation.invoke()
this.value = LoadingStatus.Success()
} catch (e: Throwable) {
this.value = LoadingStatus.Error(e)
}
}
My tests look something like this:
fun testSomething() = runTest {
viewModel.doSomething()
advanceUntilIdle()
argumentCaptor<LoadingStatus>().apply {
verify(loadingStatusObserver, atLeast(2)).onChanged(capture())
assertTrue(allValues.any { it is LoadingStatus.Loading })
assertTrue(allValues.any { it is LoadingStatus.Success })
}
}
The ViewModel contains code like this:
val loadingStatus = repository.loadingStatusObservable.asLiveData(
viewModelScope.coroutineContext + ioDispatcher
) // when running tests, ioDispatcher is a StandardTestDispatcher passed into the viewModel
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
repository.doSomething()
}
}
And the repository does something like this:
val loadingStatusObservable = MutableStateFlow<LoadingStatus>(LoadingStatus.Idle())
suspend fun doSomething() {
loadingStatusObservable.performWithStatusUpdates {
apiService.doSomethingElse()
}
}
The repository has similar tests that call doSomething() and verify that the status goes to loading and then success, and they pass, but the view model ones fail to pick up the Loading status. If I comment out the line in performWithStatusUpdates that sets the status to success after the operation, the tests do pick up the Loading status, so I'm convinced it's something to do with timing. I have run this code with print statements and debuggers and verified that the status is updating correctly, but the change isn't getting picked up by the observers.
How can I make an observer on a StateFlow.asLiveData() detect all changes, even when they're quickly followed by another, different change?
I have read the official document about Composable functions
But I can't understand Composable functions.
For example, rememberWatchState() is a Composable function in Code A, but it doesn't define my app's UI just like a normal function.
What is the difference between the Composable function and the normal function in Android?
Code A
#Composable
fun ScreenHome_Watch(
watchState:WatchState = rememberWatchState()
){
val density= watchState.density
}
#Composable
fun rememberWatchState(): WatchState {
val context: Context = LocalContext.current
val temp = loadDBWarningValueWithPreference(context)
val watchState = WatchState(temp.toFloat(),LocalDensity.current)
return remember {
watchState
}
}
class WatchState(
private val alarmValue:Float,
val density: Density
){
...
}
fun drawDial(
drawScope: DrawScope,
watchState:WatchState
) {
...
}
#Composable annotation is like a scope that gives access to Compose functions such as LaunchedEffect, SideEffect, remember or objects such as currentComposer and resembles suspend functions.
Section below is quoted from Under the hood of Jetpack Compose — part 2 of 2 article by Leland Richardson.
// function declaration
suspend fun MyFun() { … }
// lambda declaration
val myLambda = suspend { … }
// function type
fun MyFun(myParam: suspend () -> Unit) { … }
Kotlin’s suspend keyword operates on function types: you can have a function declaration that’s suspend, a lambda, or a type. Compose works in the same way: it can alter function types.
// function declaration
#Composable fun MyFun() { … }
// lambda declaration
val myLambda = #Composable { … }
// function type
fun MyFun(myParam: #Composable () -> Unit) { … }
The important point here is that when you annotate a function type with #Composable you’re changing its type: the same function type without the annotation is not compatible with the annotated type. Also, suspend functions require a calling context, meaning that you can only call suspend functions inside of another suspend function.
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // allowed
b() // NOT allowed
}
suspend
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // allowed
b() // allowed
}
Composable works the same way. This is because there’s a calling context object that we need to thread through all of the invocations.
fun Example(a: () -> Unit, b: #Composable () -> Unit) {
a() // allowed
b() // NOT allowed
}
#Composable
fun Example(a: () -> Unit, b: #Composable () -> Unit) {
a() // allowed
b() // allowed
}
what is this calling context thing that we’re passing around and why do we need to do it?
We call this object the Composer. The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.
Let’s look at an example of a counter.
#Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
This is the code that we would write, but let’s look at what the compiler does.
When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.
First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
$composer.end()
}
LazyRow or LazyColumns for instance have content with LazyListScope receiver that is not annotated with #Composable which doesn't let you to call other Composables
#Composable
fun LazyColumn(
// Rest of params
// Non Composable scope
content: LazyListScope.() -> Unit
) {
// Implementation details
}
LazyColumn(){ // LazyListScope
// You can't add Composable here because this scope is LazyListScope
// which is not annotated with #Composable, and returns compiler error
// Row(){}
items(100){
Row() {
}
}
}
You can't call Composable functions from LazyListScope but you can call inside items because its content is #Composable function.
fun item(
key: Any? = null,
contentType: Any? = null,
content: #Composable LazyItemScope.() -> Unit
) {
error("The method is not implemented")
}
You need a scope with #Composable annotation to call other #Composable functions.
It's not mandatory for Composable functions to be UI functions. Some functions such as SideEffect or LaunchedEffect are used to run when your Composable function enters composition successfully.
#Composable
#NonRestartableComposable
#OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
Or not only to check when it enters composition also to check when it exits with DisposableEffect
#Composable
private fun NonUIComposableSample() {
val context = LocalContext.current
var counter by remember { mutableStateOf(0) }
var color by remember { mutableStateOf(Color.Red) }
if (counter in 3..5) {
DisposableEffect(Unit) {
Toast.makeText(context, "Entering Composition counter: $counter", Toast.LENGTH_SHORT).show()
color = Color.Yellow
onDispose {
color = Color.Green
Toast.makeText(context, "Exiting Composition counter: $counter", Toast.LENGTH_SHORT).show()
}
}
}
Button(onClick = { counter++ }) {
Text("Counter: $counter", color = color)
}
}
In this example Block with DisposableEffect enters composition when counter is 3 stay in composition while and exits when condition is not met anymore.
Also produceState is another #Composable non ui function launches a coroutine scoped to the Composition that can push values into a returned State. Use it to convert non-Compose state into Compose state, for example bringing external subscription-driven state such as Flow, LiveData, or RxJava into the Composition.
Compose is a data structure holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.
I am writing unit tests and I have this particular case when I change observable value before executing a suspend function and then right after. I would like to write a unit test to check if the value was changes twice correctly.
Here is the method:
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
try {
viewModelScope.launch(Dispatchers.Main) {
commandLiveData.value = Pair(CANCEL_TIMER, null)
}
isProgress.set(true) //need to test it was set to true in one test
checkoutInteractor.doSomethingElse().run {
handleResult(this)
}
isProgress.set(false) //need to test it was set to false here in another test
} catch (ex: java.lang.Exception) {
handleHttpError(ex)
isProgress.set(false)
}
}
}
When writing a test I am calling doSomething() but I am unsure how to detect that the value was set to true before the checkoutInteractor.doSomethingElse call and set to false after.
Here is the test I have
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
doReturn(Response()).`when`(checkoutInteractor). checkoutInteractor.doSomethingElse()
viewModel.doSomething()
assertTrue(viewModel.isProgress.get()) //fails obviously as the value was already set to false after the `doSomethingElse` was executed.
}
}
BTW, isProgress is an ObservableBoolean
You would need to either mock or spy on isProgress and checkoutInteractor fields to record and verify the execution of their methods.
Pass a mock or spy for isProcess and checkoutInteractor into your class.
Execute doSomething()
Verify inOrder the set() and doSomethingElse() functions
Example:
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
val checkoutInteractor = Mockito.spy(CheckoutInteractor())
val isProcess = Mockito.spy(ObservableBoolean(true))
val viewModel = ViewModel(isProcess, checkoutInteractor)
viewModel.doSomething()
val inOrder = inOrder(isProcess, checkoutInteractor)
inOrder.verify(isProcess).set(true)
inOrder.verify(checkoutInteractor).doSomethingElse()
inOrder.verify(isProcess).set(false)
}
}
New at Kotlin here and trying to learn the best way to use the higher order functions and passing lambdas. I've created this method to call an API and return an object created from a string OR return a failure if something went wrong.
fun getDeviceStatus(onSuccess: (Device) -> Unit, onFailure: ((String) -> Unit)? = null) {
FuelClient.get(DEVICE_URL,
success = { responseString ->
val adapter = MoshiUtil.moshi.adapter(Device::class.java)
val deivce= adapter.fromJson(responseString)!!
onSuccess(device)
},
failure = { onFailure?.invoke(it.message!!)})
}
I can use this function fine like so:
DeviceService.getDeviceStatus(
{ w ->
print("device")
},
{ e -> print(e) })
But it bothers me a bit that I can't see the name of the functions to see what each function does. I"m wondering if there is a cleaner/better way to do this, like
DeviceService.getDeviceStatus(){
onSuccess{print("device")}
onFailure{print("error")}
}
or maybe
DeviceService.getDeviceStatus()
.onSuccess{print("device")}
.onFailure{print("error")}
But those gives errors. Any thoughts on how to best handle the onSuccess/onFailure use case that is very common? Thx
You can attach a name to each variable in kotlin. Change your code like this
DeviceService.getDeviceStatus(
onSuccess = { w ->
print("device")
},
onFailure = { e -> print(e) })
For this specific case, when the second lambda is optional, infix functions work very well:
sealed class DeviceStatusResult {
abstract infix fun onFailure(handler: (String) -> Unit)
}
class DeviceStatusSuccess(val device: Device) : DeviceStatusResult() {
override fun onFailure(handler: (String) -> Unit) = Unit
}
class DeviceStatusFailure(val errorMessage: String) : DeviceStatusResult() {
override fun onFailure(handler: (String) -> Unit) = handler(errorMessage)
}
fun getDeviceStatus(onSuccess: (Device) -> Unit): DeviceStatusResult {
// get device status
// if (success)
val device = Device()
onSuccess(device)
return DeviceStatusSuccess(device)
// else
// return DeviceStatusFailure(message)
}
Then it can used like
getDeviceStatus { device ->
println(device)
} onFailure { errorMessage ->
System.err.println(errorMessage)
}
Maybe onFailure should be called orFail or something like that.
It is good when the second argument is optional, but not so much otherwise because it doesn't force the user to actually supply a failure handler. And I don't think it's a good idea because it will be too easy to accidentally omit a failure handler. It's much better to force the user to provide one, even if it happens to be an empty one. Therefore, it is better to use named arguments for this case, even though nothing forces to actually name them.
For example we have a class which needs to have more than one function such as two functions as parameter:
class TestClass internal constructor(
private val onClickShowName: (String) -> Unit,
private val onClickShowSurname: (String) -> Unit
) { //Your work. }
Then you need to create val as TestClass:
class MainActivity {
val mTestClass = TestClass(
onClickShowName = {dataText: String -> Log.i("TEST", dataText)},
onClickShowSurname = {dataText: String -> Log.i("TEST", dataText)}
)
}