Skip to content

Commit

Permalink
Added Dao Classes and Updated Unit Tests
Browse files Browse the repository at this point in the history
Signed-off-by: Ramakrishna Joshi <ramakrishnaj995@gmail.com>
  • Loading branch information
ramakrishnajoshi committed May 2, 2022
1 parent 2890513 commit 3d1a065
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 17 deletions.
6 changes: 2 additions & 4 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/render.experimental.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 27 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion 29
Expand Down Expand Up @@ -37,11 +36,36 @@ dependencies {
testImplementation 'junit:junit:4.13.1'
//Truth gives convenient methods which makes assertions in our test cases much more readable
testImplementation 'com.google.truth:truth:1.0.1'
testImplementation "androidx.arch.core:core-testing:2.1.0"

//Android Specific Test
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' //espresso is needed for UI Tests

//If truth dependency is also needed in `androidTest` source-set then we need to add below line
androidTestImplementation 'com.google.truth:truth:1.0.1'
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
//Below dependency is needed for runBlockingTest to test coroutines methods
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"

// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.2.6"

//implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
//implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'

// Material Design
implementation 'com.google.android.material:material:1.3.0'

// Architectural Components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

// Room
implementation "androidx.room:room-runtime:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.example.unittestinginandroid.data.local

import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.example.unittestinginandroid.util.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class) //JUnit is basically used in JVM Environment,but here we are not in
// such an environment. We want this test class to run on an emulator(Android Environment) as we
// want to use Room version of our emulator/device, rather than local IDE version.
// Note: We can still test SQLite/Room using JUnit but in that case we would be using local IDE
// machine SQLite version instead of emulator/device version.
@SmallTest
class ShoppingDaoTest {

private lateinit var shoppingDatabase: ShoppingItemDatabase

private lateinit var shoppingDao: ShoppingDao

// InstantTaskExecutorRule swaps the background executor used by the Architecture Components
// with a different one which executes each task synchronously. Needed to test Architecture
// Components like LiveData
@get:Rule
val ss = InstantTaskExecutorRule()

@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()

// For Tests, we have to create a temporary database - for that we need to use the
// inMemoryDatabaseBuilder method. Information stored in an in memory database disappears
// when the process is killed.

// allowMainThreadQueries method: Room ensures that Database is never accessed on the main
// thread because it may lock the main thread and trigger an ANR. So if we try to
// access Database from main thread then we would get IllegalStateException. For testing
// we turn this check off using allowMainThreadQueries method which Disables the main
// thread query check for Room.
shoppingDatabase = Room
.inMemoryDatabaseBuilder(context, ShoppingItemDatabase::class.java)
.allowMainThreadQueries()
.build()

shoppingDao = shoppingDatabase.shoppingDao()
}

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

@Test
fun shouldSaveShoppingItemInDatabaseWhenInsertCommandIsExecuted() {
// runBlockingTest executes a [testBody] inside an immediate execution dispatcher.
// This is similar to [runBlocking] but it will immediately progress past delays and into
// [launch] and [async] blocks.
// You can use this to write tests that execute in the presence of calls to [delay] without
// causing your test to take extra time.
runBlockingTest {

val shoppingItem = ShoppingItem("name", 10, 5f, "url", 1)

shoppingDao.insertShoppingItem(shoppingItem)

// shoppingDao.observeAllShoppingItems() returns LiveData and LiveData runs asynchronously
// and we do not want that here in our test case. So we need to use getOrAwaitValue()
val shoppingItemList: List<ShoppingItem> = shoppingDao.observeAllShoppingItems().getOrAwaitValue()

assertThat(shoppingItemList).contains(shoppingItem)
}
}

@Test
fun shouldDeleteShoppingItemInDatabaseWhenDeleteQueryIsExecuted() {
runBlockingTest {
val shoppingItem1 = ShoppingItem("name", 10, 5f, "url", 1)
val shoppingItem2 = ShoppingItem("name", 10, 5f, "url", 2)

shoppingDao.insertShoppingItem(shoppingItem1)
shoppingDao.insertShoppingItem(shoppingItem2)
shoppingDao.deleteShoppingItem(shoppingItem1)

val shoppingItemList: List<ShoppingItem> = shoppingDao.observeAllShoppingItems().getOrAwaitValue()

assertThat(shoppingItemList).doesNotContain(shoppingItem1)
}
}

@Test
fun shouldReturnCorrectTotalPrice() {
runBlockingTest {
val shoppingItem1 = ShoppingItem("name", 10, 5f, "url", 1)
val shoppingItem2 = ShoppingItem("name", 10, 15f, "url", 2)

shoppingDao.insertShoppingItem(shoppingItem1)
shoppingDao.insertShoppingItem(shoppingItem2)

val totalPrice: Float = shoppingDao.observeTotalPrice().getOrAwaitValue()

assertThat(totalPrice).isEqualTo(
shoppingItem1.price * shoppingItem1.quantity + shoppingItem2.price * shoppingItem2.quantity
)
}
}
}

//FootNotes:
// Test Doubles : A test double is an object which we use in place of a real object during a test.
// Different Types Of Test Doubles are
// 1. Dummy 2. Fake 3. Stubs 4. Mocks
// Fake: Fake objects actually have a complete working implementation in them. But the
// implementation provided in them is some kind of shortcut which helps us in our task of unit
// testing, and this shortcut renders it incapable in production. A great example of this is the
// in-memory database object which we can use just for our testing purposes, while we use the
// real database object in production.
// Its important to note that a single object might act as multiple types of test double at once.
// So, a single object can act as a stub as well as a mock in the same unit test.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.unittestinginandroid.util

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

/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@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
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".TestActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,12 +1,87 @@
package com.example.unittestinginandroid

import androidx.appcompat.app.AppCompatActivity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.room.Room
import com.example.unittestinginandroid.data.local.ShoppingItem
import com.example.unittestinginandroid.data.local.ShoppingItemDatabase
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

private val TAG = this::class.java.simpleName

private val roomDatabase by lazy {
Room.databaseBuilder(this, ShoppingItemDatabase::class.java, "shopping_item_db").build()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

logInfo("Main Thread : ${Thread.currentThread()}")

insertButton.setOnClickListener {
Intent(this@MainActivity, TestActivity::class.java).also {
startActivity(it)
}
//runBlocking {
insertShoppingItem()
//}
}

roomDatabase.shoppingDao().observeTotalPrice().observe(this@MainActivity, Observer {
logInfo("Total Price: $it")
})

// Every Coroutine need to be started in a Coroutine scope
// GlobalScope means that this Coroutine will live as long as our application does
// Of course if Coroutine finishes its job, it will be destroyed and not kept alive until the
// application dies.
// GlobalScope launches Coroutine in a new thread. So whatever we write inside CoroutineScope
// will execute in a async thread.
// Coroutines are suspendable -> They can be paused and resumed.
// Coroutines have their own sleep function called delay() but with few differences.
// sleep() function of Thread class blocks/pauses the thread for particular amount of time but
// delay() function pauses a particular coroutine for particular amount of time,
// delay() does not block the thread unlike sleep() function
GlobalScope.launch(Dispatchers.IO) {
logInfo("Coroutine Running in Thread ${Thread.currentThread()}")

insertButton.text = "New thread"

networkCall()
logInfo("After Network Call")
}
}

// suspend function can be called for either another suspend function
// or from a coroutine context
private suspend fun networkCall() {
delay(5000)
logInfo("network call executed in thread : ${Thread.currentThread()}")
}

private fun insertShoppingItem() {
GlobalScope.launch {
roomDatabase.shoppingDao().insertShoppingItem(
ShoppingItem(
"Banana", 20, 1f, "imageUrl"
)
)
}
}

private fun logInfo(message: String) {
Log.d(TAG, message)
}

override fun onDestroy() {
roomDatabase.close()
super.onDestroy()
}
}
Loading

0 comments on commit 3d1a065

Please sign in to comment.