7

Unit testing a fragment with View Model in Android

 1 year ago
source link: https://medium.com/@tanaytandon/unit-testing-a-fragment-with-view-model-in-android-172fcd7807aa
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Unit testing a fragment with View Model in Android

What does Unit testing a fragment mean?

Unit testing means verifying a particular atomic piece of logic is working as it is supposed to work. Functions are the most common atomic logic pieces.A fragment is responsible for displaying data using Android Framework level classes such as EditText, TextView, ImageView, etc.Therefore unit testing a fragment means verifying that the fragment displays the data as intended.

In this post we’ll see how we can unit test a fragment which has three states:

  1. Loading: shown when view model is fetching the required data.
  2. Success: shown when data is successfully returned from the view model.
  3. Error: shown when there is an error fetching the data from the view model.

Sealed class representing the above three states

sealed class DemoDataStatus {
object Loading : DemoDataStatus()
data class Success(val data: DemoData) : DemoDataStatus()
data class Error(val msg: String) : DemoDataStatus()
}

data class DemoData(val title: String, val description: String)

app/build.gradle

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}

android {
namespace 'com.example.sampleuitesting'
compileSdk 33

defaultConfig {
applicationId "com.example.sampleuitesting"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}

packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/ASL2.0'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
exclude("META-INF/*.kotlin_module")
}

}

dependencies {

implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'

debugImplementation "androidx.test:core:1.5.0"
// Kotlin extensions for androidx.test.core
androidTestImplementation "androidx.test:core-ktx:1.5.0"
// To use the androidx.test.runner APIs
androidTestImplementation "androidx.test:runner:1.5.1"

androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation "androidx.test.espresso:espresso-intents:3.5.0"

debugImplementation "androidx.fragment:fragment-testing:1.5.4"

def nav_version = "2.5.3"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"

// dagger
def dagger_version = "2.44.2"
implementation "com.google.dagger:dagger:$dagger_version"
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

// viewmodel, lifecycle
def lifecycle_version = "2.5.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

// optional - Test helpers for LiveData
def arch_version = "2.1.0"
testImplementation "androidx.arch.core:core-testing:$arch_version"

// optional - Test helpers for Lifecycle runtime
testImplementation "androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version"

androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "io.mockk:mockk-android:1.13.3"
}

The corresponding layout file

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ProgressBar
android:id="@+id/pbDemo"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="@id/tvTitle"
app:layout_constraintStart_toStartOf="@id/tvTitle"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />

<androidx.constraintlayout.widget.Group
android:id="@+id/grpSuccess"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tvTitle,tvDescription" />

<TextView
android:id="@+id/tvError"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:textColor="@android:color/holo_red_dark"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btnRetry"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/tvError"
app:layout_constraintStart_toStartOf="@id/tvError"
app:layout_constraintTop_toBottomOf="@id/tvError" />

<androidx.constraintlayout.widget.Group
android:id="@+id/grpError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="btnRetry,tvError" />

</androidx.constraintlayout.widget.ConstraintLayout>

The view model contract that is consumed by the fragment

open class DemoViewModel : ViewModel() {
fun fetchInfo(): Flow<DemoDataStatus> {
return flow{
// insert business logic to fetch data and emit corresponding DemoDataStatus classes
}
}
}

The fragment class

class DemoFragment : Fragment() {

private var _binding: FragmentDemoBinding? = null
private val mBinding get() = _binding!!

private val mViewModel: DemoViewModel by lazy {
ViewModelProvider(requireActivity())[DemoViewModel::class.java]
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDemoBinding.inflate(inflater)
return mBinding.root
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fetchInfo()
mBinding.btnRetry.setOnClickListener {
fetchInfo()
}
}

private fun fetchInfo() {
viewLifecycleOwner.lifecycleScope.launch {
mViewModel.fetchInfo().collect {
when (it) {
is DemoDataStatus.Loading -> {
// show loading views and hide success and error views
}
is DemoDataStatus.Success -> {
// show success view and hide loading and error views
}
is DemoDataStatus.Error -> {
// show error view and hide success and loading views
}
else -> {}
}
}
}
}

}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK