막무가내 EVERYTHING

막무가내 EVERYTHING

막무가내 EVERYTHING

IT

Android UnitTest example and summary

막무가내EVERYTHING 2022. 11. 28. 21:02
728x90
반응형

 

 

 

I'm going to post a summary of the basics of Android unit testing.

 

 

 

There are two types of unit tests in Android: instrumentation test (androidTest) and local unit test (test).

To put it simply, androidTest is a test that has dependencies on the Android framework.

tests are tests that can be done regardless of the Android framework. For example, writing algorithm test code in general IntelliJ. All you need is JVM.

For a detailed explanation of this, please refer to the following :)

 

 

https://developer.android.com/studio/test/test-in-android-studio

 

 

These are the libraries needed for Android unit testing in this post.

    testImplementation 'junit:junit:4.13'
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
    testImplementation "com.google.truth:truth:1.0.1"
    testImplementation 'androidx.test.ext:junit:1.1.2'
    testImplementation "org.robolectric:robolectric:4.4" 


    androidTestImplementation "junit:junit:4.13"
    androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
    androidTestImplementation "com.google.truth:truth:1.0.1"
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

 

 

 

First, let's take a look at the local unit test (test), which is a test that does not require the Android framework and runs on the JVM, along with practice code.

 


1. First Example (Normal Class + Junit4 + Truth)

 

This is the MyCalc class to be unit tested.

These functions take a radius as a parameter and return the circumference and area of ​​a circle.

 

[class to test]

class MyCalc : Calculations {

    private val pi = 3.14

    override fun calculateCircumference(radius: Double): Double {
        return 2 * pi * radius
    }

    override fun calculateArea(radius: Double): Double {
        return pi * radius * radius
    }
}

[test code]

import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test

class MyCalcTest{
    private lateinit var myCalc: MyCalc

    @Before
    fun setUp() {
        myCalc = MyCalc()
    }

    @Test
    fun calculateCircumference_radiusGiven_returnsCorrectResult(){
       val result = myCalc.calculateCircumference(2.1)
       assertThat(result).isEqualTo(13.188)

    }

    @Test
    fun calculateCircumference_zeroRadius_returnsCorrectResult(){
        val result = myCalc.calculateCircumference(0.0)
        assertThat(result).isEqualTo(0.0)

    }


    @Test
    fun calculateArea_radiusGiven_returnsCorrectResult(){
        val result = myCalc.calculateArea(2.1)
        assertThat(result).isEqualTo(13.8474)

    }

    @Test
    fun calculateArea_zeroRadius_returnsCorrectResult(){
        val result = myCalc.calculateCircumference(0.0)
        assertThat(result).isEqualTo(0.0)

    }

}

 

 

If you do not know how to create a test code, you can refer to the following pictures.

 

클래스에 Alt + Enter
Junit4

 

 

If you look at the photo of how to generate the above test code and the test code (even though there is no After)

There are @Before and @After. This is the function that will be executed before and after all test functions in the unit test class are executed.

It's easy to understand that it's onCreate(), onDestory() in Android.



The simple summary of the test process implemented here is as follows.



1. Since we are going to test the MyCalc() class first, create the class in advance in @Before.



2. Then, after calling the desired function in the function of the class, use the functions such as assertThat() and isEqualTo() of the Truth testing library to test whether the value you want comes out. In assertThat(), you can put the value you want to test.

 

isEqultTo() and many functions.

 

3. If the test passed, it will be green and if it fails, the red result will be checked. End

 

 

Looking a little more into the Truth library:

import com.google.common.truth.Truth.assertThat

 assertThat(result).isEqualTo(0.0)
androidTestImplementation "com.google.truth:truth:1.0.1"

Truth is one of the assertion testing libraries developed by the Guava team. You can use it instead of Junit4, which you have used so far, and you can create various functions and easy-to-read testing code that looks good. (I also added a Junit4 test code example below as a taster!) Below are examples and explanations from Google Docs. In the case of Google's example, Truth is imported, so you can omit Truth and use it as assertThat.

 

https://developer.android.com/training/testing/local-tests

 

Note that depending on Truth's assertThat() parameter, a different testing function will result. I just added 

 

 

 

Value verification tests are possible with Junit4's Assert as well as Truth. I have attached as an example what I used in another project.

 

 

 

 

 

 

 

Additionally, unit tests also have naming conventions. Please refer to the link below for naming conventions when writing Unit Tests. In the test code example above, the test code naming convention shown in the picture below was used.

medium.com/@stefanovskyi/unit-test-naming-conventions-dd9208eadbea

 

Unit Test Naming Conventions

Test naming is important for teams on long term project as any other code style conventions. By applying code convention in tests you…

medium.com

 

여기 테스트코드에서는 위 네이밍컨벤션 종류 중 이것을 참고했습니다.

 

 

 

 

 

 

We have seen the basic test of the normal class above.

 

 


2. Second Example (ViewModel + LiveData + Mockito + Junit4 + Truth)

 

Second, let's take a look at the ViewModel testing method, which is often used in Android.

Usually, ViewModel is a class that has logic and values, so actual Android testing is also the most active and important. After the ViewModel function is called, it is tested to see if the value is set well in LiveData or if an error occurs.

The following is an example code of ViewModel to be tested. Just like the previous example, you can think of it as adding logic that calculates the diameter and area of a circle and sets it to live data.

 

[class to test]

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.lang.Exception

class CalcViewModel(
    private val calculations: Calculations
) : ViewModel() {

    var radius = MutableLiveData<String>()

    var area = MutableLiveData<String>()
    val areaValue: LiveData<String>
        get() = area

    var circumference = MutableLiveData<String>()
    val circumferenceValue: LiveData<String>
        get() = circumference


    fun calculate() {

        try {

            val radiusDoubleValue = radius.value?.toDouble()
            if (radiusDoubleValue != null) {
                calculateArea(radiusDoubleValue)
                calculateCircumference(radiusDoubleValue)
            } else {
                area.value = null
                circumference.value = null
            }

        } catch (e: Exception) {
            Log.i("MYTAG", e.message.toString())
            area.value = null
            circumference.value = null
        }

    }

    fun calculateArea(radius: Double) {
        val calculatedArea = calculations.calculateArea(radius)
        area.value = calculatedArea.toString()
    }

    fun calculateCircumference(radius: Double) {
        val calculatedCircumference = calculations.calculateCircumference(radius)
        circumference.value = calculatedCircumference.toString()
    }

}

 

 

Here, too, we have written unit tests in the test directory.

[test code]

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito

class CalcViewModelTest{

    private lateinit var calcViewModel: CalcViewModel
    private lateinit var calculations: Calculations

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()


    @Before
    fun setUp() {
        calculations = Mockito.mock(Calculations::class.java)
        Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474)
        Mockito.`when`(calculations.calculateCircumference(1.0)).thenReturn(6.28)
        calcViewModel = CalcViewModel(calculations)
    }

    @Test
    fun calculateArea_radiusSent_updateLiveData(){
       calcViewModel.calculateArea(2.1)
       val result = calcViewModel.areaValue.value
       assertThat(result).isEqualTo("13.8474")
    }

    @Test
    fun calculateCircumference_radiusSent_updateLiveData(){
        calcViewModel.calculateCircumference(1.0)
        val result = calcViewModel.circumferenceValue.value
        assertThat(result).isEqualTo("6.28")
    }
}

 

 

 

Let's take a look at the differences from the first unit test.

 

1. First, @get:Rule .

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

Rule It literally adds a rule. The rule set above makes all the architectural components related to the background task run on the same (single) thread so that the test results are executed synchronously.

In other words, it makes all tasks synchronous, so you don't have to worry about synchronization, and you can think of it as essential when testing LiveData.

Please refer to the following for details

 

 

https://developer.android.com/reference/android/arch/core/executor/testing/InstantTaskExecutorRule

 

 

 

 

 

 

 

 

2. The second is Mockito. Mockito is one of Test Doubles (where Double means double), and Mock is an object that specifies expectations for invocation and is programmed to behave according to those contents.

To put it simply, you can think of mocking the necessary objects for smooth testing and deciding whether to return these when calling any function of this object. However, the type of the parameter or return value must match (I think I control how the object behaves at will!!)

At this time, Mockito usually uses interfaces, not implementation objects. (I had a hard time with an error when I put an object that implemented an interface into Mockito when. I solved it by putting an interface. If you know the cause, please comment.

Regarding TestDouble, you have well organized the following Korean site. please note :)

brunch.co.kr/@tilltue/55

 

Test Doubles 정리

Dummy, Fake, Stub, Spy, Mock | 테스트 더블이란? 실제 객체를 대신해서 테스팅에서 사용하는 모든 방법을 일컬여 호칭하는 것이다. (영화 촬영시 위험한 역활을 대신하는 스턴트 더블에서 비롯되었다.)

brunch.co.kr

 

I wrote a comment about the Mockito code. :)

    //참고 : Mockito import 시 Mockito 는 생략가능합니다.
    @Before
    fun setUp() {
    	//내가 Mocking 할 클래스를 먼저 mock() 해준다.
        calculations = Mockito.mock(Calculations::class.java)
        //Mocking한 클래스를 `when`()안에 실행할 함수를 호출하고 
        //thenReturn()에는 내가 `when`과 똑같은 함수 호출시 반환할 값을 명시해준다.
        Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474)
        Mockito.`when`(calculations.calculateCircumference(1.0)).thenReturn(6.28)
        calcViewModel = CalcViewModel(calculations)
    }

 

Mocktio도 다양한 함수를 제공합니다. 에러를 던질수도 있고

 

 

 


3. Third example (ViewModel + LiveData + Asynchronous data fetch function test + getOrAwaitValue() + Mockito + Junit4 + Truth)

 

[class to test]

We will conduct a test to see if the LiveData value is set well when data is asynchronously fetched from UseCase (or DataSource, Repository) in the view model. Unlike the previous example, the difference is that you have to load data asynchronously, set it in live data, and observe it.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.anushka.tmdbclient.domain.usecase.GetMoviesUseCase
import com.anushka.tmdbclient.domain.usecase.UpdateMoviesUsecase

class MovieViewModel(
    private val getMoviesUseCase: GetMoviesUseCase,
    private val updateMoviesUsecase: UpdateMoviesUsecase
) : ViewModel() {

    fun getMovies() = liveData {
        val movieList = getMoviesUseCase.execute()
        emit(movieList)
    }

    fun updateMovies() = liveData {
        val movieList = updateMoviesUsecase.execute()
        emit(movieList)
    }

}

 

 

[Add LiveDataTestUtil.kt]

This is the key point of this example.



Testing is possible by adding the following LiveData extension function to the data received asynchronously.

This function returns immediately if the corresponding live data has a value, observes the live data until a new value is received, and releases the observing when received. (By default, it waits for 2 seconds, and if the value is not set after 2 seconds, an exception is raised. The time can be adjusted as a constructor parameter.)



Even Google recommends using this.



For more details, please refer to the following site :)

(참고 : medium.com/androiddevelopers/unit-testing-livedata-and-other-common-observability-problems-bb477262eb04 )

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

 

 

[FakeRepository]

Make the repository of the Data layer necessary for testing fake.

import com.anushka.tmdbclient.data.model.movie.Movie
import com.anushka.tmdbclient.domain.repository.MovieRepository

class FakeMovieRepository : MovieRepository {
    private val movies = mutableListOf<Movie>()

    init {
        movies.add(Movie(1, "overview1", "path1", "date1", "title1"))
        movies.add(Movie(2, "overview2", "path2", "date2", "title2"))
    }

    override suspend fun getMovies(): List<Movie>? {
        return movies
    }

    override suspend fun updateMovies(): List<Movie>? {
        movies.clear()
        movies.add(Movie(3, "overview3", "path3", "date3", "title3"))
        movies.add(Movie(4, "overview4", "path4", "date4", "title4"))
        return movies
    }
}

 

 

 

[test code] 

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.anushka.tmdbclient.data.model.movie.Movie
import com.anushka.tmdbclient.data.repository.movie.FakeMovieRepository
import com.anushka.tmdbclient.domain.usecase.GetMoviesUseCase
import com.anushka.tmdbclient.domain.usecase.UpdateMoviesUsecase
import com.anushka.tmdbclient.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MovieViewModelTest{

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: MovieViewModel

    @Before
    fun setUp() {
      val fakeMovieRepository = FakeMovieRepository()
      val getMoviesUsecase = GetMoviesUseCase(fakeMovieRepository)
      val updateMoviesUsecase = UpdateMoviesUsecase(fakeMovieRepository)
      viewModel = MovieViewModel(getMoviesUsecase,updateMoviesUsecase)
    }

    @Test
    fun getMovies_returnsCurrentList(){
        val movies = mutableListOf<Movie>()
        movies.add(Movie(1, "overview1", "path1", "date1", "title1"))
        movies.add(Movie(2, "overview2", "path2", "date2", "title2"))

        val currentList = viewModel.getMovies().getOrAwaitValue()
        assertThat(currentList).isEqualTo(movies)

    }

    @Test
    fun updateMovies_returnsUpdatedList(){
        val movies = mutableListOf<Movie>()
        movies.add(Movie(3, "overview3", "path3", "date3", "title3"))
        movies.add(Movie(4, "overview4", "path4", "date4", "title4"))

        val updatedList = viewModel.updateMovies().getOrAwaitValue()
        assertThat(updatedList).isEqualTo(movies)

    }
}

 

Unlike the previous example, there are @RunWith(AndroidJUnit4::class) and Truth that you can see for the first time.

 

 


So far we have looked at local unit testing (test), now let's look at an example of an instrumentation test (androidTest) that depends on Android.

 

1. Room Unit Test (ViewModel + LiveData + Mockito + Junit4)

 

Since Room DB is one of the Android Jetpack Components and requires Android dependencies, test code must be created in the AnroidTest directory for Room unit testing.

Let's test whether the query of Room DB's DAO class works properly

 

[test to Room DAO Class]

@Dao
interface MovieDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveMovies(movies : List<Movie>)

@Query("DELETE FROM popular_movies")
suspend fun deleteAllMovies()

@Query("SELECT * FROM popular_movies")
suspend fun getMovies():List<Movie>
}

 

[Room Unit Test]

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Database
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.anushka.tmdbclient.data.model.movie.Movie
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MovieDaoTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var dao: MovieDao
    private lateinit var database: TMDBDatabase

    @Before
    fun setUp() {
     database = Room.inMemoryDatabaseBuilder(
         ApplicationProvider.getApplicationContext(),
         TMDBDatabase::class.java
     ).build()
     dao = database.movieDao()
    }

    @After
    fun tearDown() {
        database.close()
    }

    @Test
    fun saveMoviesTest() = runBlocking{
        val movies = listOf(
            Movie(1,"overview1","posterPath1","date1","title1"),
            Movie(2,"overview2","posterPath2","date2","title2"),
            Movie(3,"overview3","posterPath3","date3","title3")
        )
        dao.saveMovies(movies)

        val allMovies = dao.getMovies()
        Truth.assertThat(allMovies).isEqualTo(movies)
    }


    @Test
    fun deleteMoviesTest() = runBlocking {
        val movies = listOf(
            Movie(1,"overview1","posterPath1","date1","title1"),
            Movie(2,"overview2","posterPath2","date2","title2"),
            Movie(3,"overview3","posterPath3","date3","title3")
        )
        dao.saveMovies(movies)
        dao.deleteAllMovies()
        val moviesResult = dao.getMovies()
        Truth.assertThat(moviesResult).isEmpty()
    }
}

The Room test code process is as follows.

@Before creates Room database and DAO in advance.

After that, after testing the Room DAO methods, close() the database connection in @After.



In addition to the previously covered ones, let's take a look at the new ones one by one. I won't go into what a room is or anything like that because that's off topic.

 

 

1. @RunWith(AndroidJUnit4::class)

@RunWith(AndroidJUnit4::class)
class MovieDaoTest {

The concept is also best to look at the documentation. please see below

 

 

https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/runner

 

 

 

2.runBlocking

The runBlocking() function is one of the simplest ways to wait for an operation within a block of code to complete, and is added to allow room test codes to proceed sequentially.

 

 

 

 

 

 

 

 

반응형