Hey there! In this article, we’ll dive into how to use MockK to test Android-specific components like ViewModel
, LiveData
, Activity
, and Fragment
. We’ll also explore integrating MockK with libraries like JUnit and Coroutines, and cover how to test asynchronous code with Flows and suspend functions.
Let’s get started!
Mocking ViewModels and LiveData
Why Test ViewModels?
ViewModels are a crucial part of Android’s architecture for managing UI-related data. Testing ViewModels ensures that your business logic is correct and that LiveData updates as expected.
Example of Mocking a ViewModel with MockK
Let’s say you have a UserViewModel
that fetches user data and exposes it via LiveData:
class UserViewModel(private val userRepository: UserRepository) : ViewModel() { val userName = MutableLiveData<String>() fun loadUser(userId: Int) { val name = userRepository.getUser(userId) userName.value = name } }
Here’s how to test this ViewModel using MockK and JUnit:
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test class UserViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() @Test fun `test loadUser updates userName LiveData`() { val mockUserRepository = mockk<UserRepository>() val viewModel = UserViewModel(mockUserRepository) val observer = mockk<Observer<String>>(relaxed = true) // Stub the repository every { mockUserRepository.getUser(1) } returns "John Doe" // Observe the LiveData viewModel.userName.observeForever(observer) // Call the function viewModel.loadUser(1) // Verify the LiveData was updated verify { observer.onChanged("John Doe") } } }
Key Points
- Use
InstantTaskExecutorRule to execute LiveData tasks synchronously during testing.
- Mock observers with
mockk<Observer<T>>()
. - Verify LiveData changes using
verify { observer.onChanged(value) }
.
Testing Activities and Fragments with MockK
Why Test Activities and Fragments?
Activities and Fragments are central to your app’s UI and user interaction. Testing them ensures that they behave correctly, especially when interacting with ViewModels, LiveData, and other components.
Mocking Dependencies in an Activity
Let’s say you have an Activity
that displays user details via a UserViewModel
:
class UserActivity : AppCompatActivity() { private val viewModel: UserViewModel by lazy { ViewModelProvider(this).get(UserViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user) viewModel.userName.observe(this) { name -> findViewById<TextView>(R.id.userNameTextView).text = name } } }
Testing the Activity
Here’s how to test this Activity
using MockK and JUnit:
import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ActivityScenario import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals import org.junit.Test class UserActivityTest { @Test fun `test UserActivity displays user name correctly`() { // Mock the ViewModel val mockViewModel = mockk<UserViewModel>(relaxed = true) val fakeLiveData = MutableLiveData<String>() every { mockViewModel.userName } returns fakeLiveData // Launch the Activity val scenario = ActivityScenario.launch(UserActivity::class.java) scenario.onActivity { activity -> // Inject the mocked ViewModel activity.viewModel = mockViewModel fakeLiveData.value = "Jane Doe" val textView = activity.findViewById<TextView>(R.id.userNameTextView) assertEquals("Jane Doe", textView.text) } } }
- Use
ActivityScenario to launch and interact with Activities.
- Inject mocks into the Activity to isolate it from real dependencies.
- Verify the UI updates as expected.
Integrating MockK with Coroutines and JUnit
Why Test Coroutines?
Coroutines make asynchronous programming easier, but they also introduce complexities in testing. MockK provides coEvery
and coVerify
to handle suspending functions.
Example of Testing a Coroutine Function
Suppose you have a UserRepository
with a suspend function:
class UserRepository { suspend fun getUser(id: Int): String = "Real User" }
Here’s how to test a ViewModel that uses this repository:
class UserViewModel(private val userRepository: UserRepository) : ViewModel() { val userName = MutableLiveData<String>() fun loadUser(userId: Int) { viewModelScope.launch { val name = userRepository.getUser(userId) userName.value = name } } }
Test with MockK and Coroutines
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test class UserViewModelCoroutineTest { @get:Rule val rule = InstantTaskExecutorRule() @Test fun `test loadUser updates userName LiveData with coroutine`() = runTest { val mockRepository = mockk<UserRepository>() val viewModel = UserViewModel(mockRepository) coEvery { mockRepository.getUser(1) } returns "John Doe" viewModel.loadUser(1) coVerify { mockRepository.getUser(1) } assertEquals("John Doe", viewModel.userName.value) } }
- Use
coEvery
andcoVerify
for suspending functions. - Use
runTest
to test coroutine code. Dispatchers.Main
can be overridden withDispatchers.Unconfined
during tests.
Job Offers
Testing Flows with MockK
Flows are a powerful way to handle streams of data. Testing Flows can be tricky, but MockK and kotlinx.coroutines provide tools to make it easier.
Example of Testing a Flow
Suppose you have a repository that returns a Flow
of user names:
class UserRepository { fun getUserFlow(): Flow<String> = flow { emit("John Doe") } }
Test the Flow
import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test class UserRepositoryFlowTest { @Test fun `test getUserFlow emits correct value`() = runTest { val mockRepository = mockk<UserRepository>() coEvery { mockRepository.getUserFlow() } returns flowOf("Jane Doe") val result = mockRepository.getUserFlow().toList() assertEquals(listOf("Jane Doe"), result) } }
Key Points
- Use
flowOf
to create simple Flow mocks. - Use
toList()
to collect Flow emissions for assertions.
Conclusion
You’ve learned how to:
- Mock ViewModels, LiveData, Activities, and Fragments with MockK.
- Integrate MockK with JUnit and Coroutines.
- Test asynchronous code using
coEvery
,coVerify
, and Flows.
With these skills, you’re well-equipped to write robust and reliable unit tests for your Android apps. Happy testing!
This article is previously published on proandroiddev.com.