-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Dao Classes and Updated Unit Tests
Signed-off-by: Ramakrishna Joshi <ramakrishnaj995@gmail.com>
- Loading branch information
1 parent
2890513
commit 3d1a065
Showing
15 changed files
with
397 additions
and
17 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
app/src/androidTest/java/com/example/unittestinginandroid/data/local/ShoppingDaoTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
47 changes: 47 additions & 0 deletions
47
app/src/androidTest/java/com/example/unittestinginandroid/util/LiveDataUtilAndroidTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 76 additions & 1 deletion
77
app/src/main/java/com/example/unittestinginandroid/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.