Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document configuration for AGP to unit test Molecule #114

Closed
punitda opened this issue Sep 14, 2022 · 5 comments · Fixed by #119
Closed

Document configuration for AGP to unit test Molecule #114

punitda opened this issue Sep 14, 2022 · 5 comments · Fixed by #119
Labels
documentation Improvements or additions to documentation enhancement New feature or request PR welcome

Comments

@punitda
Copy link
Contributor

punitda commented Sep 14, 2022

I'm trying out Molecule with AAC ViewModel. Here is how the VM code is written exposing stateFlow stream using launchMolecule()

Molecule version used : 0.5.0-SNAPSHOT

@HiltViewModel
class PhotosListViewModel @Inject constructor(
    private val unsplashRepository: UnsplashRepository,
    @MoleculeScope private val scope: CoroutineScope,
    @CompositionClock private val clock: RecompositionClock,
) : ViewModel() {

    private val events = Channel<Event>()
    val stateFlow = scope.launchMolecule(clock = clock) {
        present(events.receiveAsFlow())
    }

    init {
        processEvent(InitialPageEvent)
    }

    @Composable
    fun present(events: Flow<Event>): PhotosListUIState {
        var isLoading by remember { mutableStateOf(false) }
        var error by remember { mutableStateOf<String?>(null) }
        val images = remember { mutableStateListOf<UnsplashImage>() }

        LaunchedEffect(Unit) {
            events.collect { event ->
                when (event) {
                    InitialPageEvent -> {
                        isLoading = true
                        error = null
                        when (val result =
                            unsplashRepository.getPhotos(page = 1, perPage = ITEM_PER_PAGE)) {
                            is Error -> {
                                isLoading = false
                                error = result.message
                            }
                            is Success -> {
                                isLoading = false
                                images.addAll(result.data.images)
                            }
                        }

                    }
                }
            }
        }
        return PhotosListUIState(
            isLoading = isLoading,
            error = error,
            images = images,
        )
    }

    fun processEvent(event: Event) {
        scope.launch {
            events.send(event)
        }
    }
}

// UI State
data class PhotosListUIState(
    val isLoading: Boolean = false,
    val error: String? = null,
    val images: List<UnsplashImage> = emptyList(),
)

// Events
sealed interface Event
object InitialPageEvent : Event

App works fine on Android devices without any issue when VM is provided with correct coroutineScope and frameClock. However, when I write Unit test for VM it requires android.os.Trace to be mocked. Added both unit test and it's error log below.

VM's Unit Test

import app.cash.molecule.RecompositionClock
import app.cash.turbine.testIn
import dev.punitd.unplashapp.data.fake.FakeUnsplashRepository
import dev.punitd.unplashapp.model.images
import dev.punitd.unplashapp.model.pageLinks
import dev.punitd.unplashapp.util.CoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test


@OptIn(ExperimentalCoroutinesApi::class)
class PhotosListViewModelTest {

    @get:Rule
    val coroutineRule = CoroutineRule()

    @Test
    fun successStateIfApiSucceeds() = runTest {
        val viewModel = PhotosListViewModel(
            unsplashRepository = FakeUnsplashRepository(coroutineRule.testDispatcher),
            scope = this, // TestScope
            clock = RecompositionClock.Immediate,
        )

        // InitialPageEvent is send inside VM's init{} block by default
        // That's why we're not sending any events here.

        val turbine = viewModel.stateFlow.testIn(this)
        assertEquals(PhotosListUIState(isLoading = false), turbine.awaitItem())
        assertEquals(PhotosListUIState(isLoading = true), turbine.awaitItem())
        assertEquals(
            PhotosListUIState(
                isLoading = false,
                error = null,
                images = images,
                pageLinks = pageLinks
            ),
            turbine.awaitItem()
        )
        turbine.cancel()
    }
}

Test Error log

Method beginSection in android.os.Trace not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method beginSection in android.os.Trace not mocked. See http://g.co/androidstudio/not-mocked for details.
	at android.os.Trace.beginSection(Trace.java)
	at androidx.compose.runtime.Trace.beginSection(ActualAndroid.android.kt:30)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:4357)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3119)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:584)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:811)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
	at app.cash.molecule.MoleculeKt.launchMolecule(molecule.kt:163)
	at app.cash.molecule.MoleculeKt.launchMolecule(molecule.kt:108)
	at dev.punitd.unplashapp.screen.photos.PhotosListViewModel.<init>(PhotosListViewModel.kt:32)

I'm sure i'm doing something stupid here because unit tests written in sample app of this repo works just fine without any such error.

Questions:

  • Is there something I need to mock which i'm missing here?
  • Are Coroutinescope and FrameClock passed to VM in test incorrect?

Repo to reproduce it : https://github.com/punitda/Tinysplash

Thanks!

@punitda punitda changed the title Unit testing molecule requires android.os.Trace to be mocked Unit testing launchMolecule requires android.os.Trace to be mocked Sep 14, 2022
@JakeWharton
Copy link
Member

You need to set

android {
  testOptions {
    unitTests.returnDefaultValues = true
  }
}

per that link. This requirement comes from Compose itself, and not anything this library is doing. All usages of Compose in Android unit tests will require this be set.

@punitda
Copy link
Contributor Author

punitda commented Sep 14, 2022

@JakeWharton Got it! That link in the error log wasn't opening on my end. Sorry for the trouble.
Tests are working now after setting suggested config option. Thank you! 👍

@punitda punitda closed this as completed Sep 14, 2022
@JakeWharton
Copy link
Member

Oh, you're right. I'll file a bug on AGP for that.

@JakeWharton
Copy link
Member

We can also probably add this to our docs.

@JakeWharton JakeWharton reopened this Sep 14, 2022
@JakeWharton JakeWharton changed the title Unit testing launchMolecule requires android.os.Trace to be mocked Document configuration for AGP to unit test Molecule Sep 14, 2022
@JakeWharton JakeWharton added documentation Improvements or additions to documentation enhancement New feature or request PR welcome labels Sep 14, 2022
@JakeWharton
Copy link
Member

Upstream bug about exception message: https://issuetracker.google.com/issues/246752914

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request PR welcome
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants