Skip to content

Commit

Permalink
[#74] Create HomeScreenUITest
Browse files Browse the repository at this point in the history
  • Loading branch information
Wadeewee committed Feb 6, 2023
1 parent 3a40ac2 commit 5d95fc7
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 5 deletions.
16 changes: 14 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ android {
targetSdk = Versions.ANDROID_TARGET_SDK_VERSION
versionCode = Versions.ANDROID_VERSION_CODE
versionName = Versions.ANDROID_VERSION_NAME
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
Expand Down Expand Up @@ -103,6 +104,12 @@ android {
xmlOutput = file("build/reports/lint/lint-result.xml")
}

packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}

testOptions {
unitTests {
isIncludeAndroidResources = true
Expand Down Expand Up @@ -153,6 +160,11 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling:${Versions.COMPOSE_VERSION}")

// Testing
androidTestImplementation("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}")
androidTestImplementation("io.mockk:mockk-android:${Versions.TEST_MOCKK_VERSION}")
androidTestImplementation("io.mockk:mockk-agent-android:${Versions.TEST_MOCKK_VERSION}")
androidTestImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}")

testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}")
testImplementation("junit:junit:${Versions.TEST_JUNIT_VERSION}")
testImplementation("androidx.test:core:${Versions.TEST_ANDROIDX_CORE_VERSION}")
Expand All @@ -164,8 +176,8 @@ dependencies {
testImplementation("io.mockk:mockk:${Versions.TEST_MOCKK_VERSION}")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.TEST_COROUTINES_VERSION}")
testImplementation("app.cash.turbine:turbine:${Versions.TEST_TURBINE_VERSION}")
testImplementation ("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}")
testImplementation ("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}")
testImplementation("androidx.compose.ui:ui-test-junit4:${Versions.COMPOSE_VERSION}")
testImplementation("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}")

kaptTest("com.google.dagger:hilt-android-compiler:${Versions.HILT_VERSION}")
testAnnotationProcessor("com.google.dagger:hilt-android-compiler:${Versions.HILT_VERSION}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package co.nimblehq.compose.crypto.test

import co.nimblehq.compose.crypto.util.DispatchersProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineTestRule : TestWatcher() {

internal val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()

override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}

val testDispatcherProvider = object : DispatchersProvider {
override val io: CoroutineDispatcher
get() = testDispatcher
override val main: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
}
}

@OptIn(ExperimentalCoroutinesApi::class)
fun CoroutineTestRule.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
testDispatcher.runBlockingTest(block)
}
100 changes: 100 additions & 0 deletions app/src/androidTest/java/co/nimblehq/compose/crypto/test/MockUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package co.nimblehq.compose.crypto.test

import co.nimblehq.compose.crypto.domain.model.CoinDetail
import co.nimblehq.compose.crypto.domain.model.CoinItem
import java.math.BigDecimal

object MockUtil {

val myCoins = listOf(
CoinItem(
id = "bitcoin",
symbol = "btc",
coinName = "Bitcoin",
image = "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579",
currentPrice = BigDecimal(21953),
marketCap = BigDecimal(418632879244),
marketCapRank = 1,
fullyDilutedValuation = BigDecimal(394474286491),
totalVolume = BigDecimal(40284988945),
high24h = BigDecimal(23014),
low24h = BigDecimal(21175),
priceChange24h = BigDecimal(777.55),
priceChangePercentage24h = 3.67201,
marketCapChange24h = BigDecimal(15300446085.0),
marketCapChangePercentage24h = 3.79351,
circulatingSupply = BigDecimal(19143668),
totalSupply = BigDecimal(21000000),
maxSupply = BigDecimal(21000000),
ath = BigDecimal(69045),
athChangePercentage = -68.93253,
athDate = "2021-11-10T14:24:19.604Z",
atl = BigDecimal(0.0398177),
atlChangePercentage = 661256.26362,
atlDate = "2017-10-19T00:00:00.000Z",
roi = CoinItem.RoiItem(
times = BigDecimal(106.82921216576392),
currency = "btc",
percentage = 10682.921216576393
),
lastUpdated = "2022-09-07T05:38:22.556Z",
priceChangePercentage24hInCurrency = 3.672009841642702
)
)

val trendingCoins = myCoins

val coinDetail = CoinDetail(
id = "bitcoin",
symbol = "btc",
coinName = "Bitcoin",
image = CoinDetail.Image(
large = "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579",
small = "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579",
thumb = "https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579"
),
marketData = CoinDetail.MarketData(
currentPrice = mapOf("usd" to BigDecimal(19112.45)),
ath = mapOf("usd" to BigDecimal(69045)),
athChangePercentage = mapOf("usd" to -72.30426),
athDate = emptyMap(),
atl = mapOf("usd" to BigDecimal(67.81)),
atlChangePercentage = mapOf("usd" to 28100.4782),
atlDate = emptyMap(),
marketCap = mapOf("usd" to BigDecimal(366436890217)),
marketCapRank = 0,
fullyDilutedValuation = emptyMap(),
totalVolume = emptyMap(),
high24h = emptyMap(),
low24h = emptyMap(),

priceChange24h = BigDecimal.ZERO,
priceChangePercentage24h = 0.0,
priceChangePercentage7d = 0.0,
priceChangePercentage14d = 0.0,
priceChangePercentage30d = 0.0,
priceChangePercentage60d = 0.0,
priceChangePercentage200d = 0.0,
priceChangePercentage1y = 0.0,
marketCapChange24h = BigDecimal.ZERO,
marketCapChangePercentage24h = 1.0166,

priceChange24hInCurrency = emptyMap(),
priceChangePercentage24hInCurrency = mapOf("usd" to 0.74874),
priceChangePercentage7dInCurrency = emptyMap(),
priceChangePercentage14dInCurrency = emptyMap(),
priceChangePercentage30dInCurrency = emptyMap(),
priceChangePercentage60dInCurrency = emptyMap(),
priceChangePercentage200dInCurrency = emptyMap(),
priceChangePercentage1yInCurrency = emptyMap(),
marketCapChange24hInCurrency = emptyMap(),
marketCapChangePercentage24hInCurrency = emptyMap(),

totalSupply = BigDecimal.ZERO,
maxSupply = BigDecimal.ZERO,
circulatingSupply = BigDecimal.ZERO,

lastUpdated = "lastUpdated"
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.nimblehq.compose.crypto.ui

import co.nimblehq.compose.crypto.test.CoroutineTestRule
import co.nimblehq.compose.crypto.test.runBlockingTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Rule

@ExperimentalCoroutinesApi
abstract class BaseScreenTest {

@get:Rule
private var coroutineRule = CoroutineTestRule()

protected fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
coroutineRule.runBlockingTest(block)

protected val testDispatcherProvider = coroutineRule.testDispatcherProvider

protected val testDispatcher = coroutineRule.testDispatcher
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package co.nimblehq.compose.crypto.ui.screen

import androidx.activity.compose.setContent
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import co.nimblehq.compose.crypto.test.MockUtil
import co.nimblehq.compose.crypto.R
import co.nimblehq.compose.crypto.domain.usecase.GetMyCoinsUseCase
import co.nimblehq.compose.crypto.domain.usecase.GetTrendingCoinsUseCase
import co.nimblehq.compose.crypto.extension.toFormattedString
import co.nimblehq.compose.crypto.ui.BaseScreenTest
import co.nimblehq.compose.crypto.ui.navigation.AppDestination
import co.nimblehq.compose.crypto.ui.screens.MainActivity
import co.nimblehq.compose.crypto.ui.screens.home.*
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@ExperimentalCoroutinesApi
class HomeScreenUITest : BaseScreenTest() {

@get:Rule
val composeAndroidTestRule = createAndroidComposeRule<MainActivity>()

private val homeTitle: String
get() = composeAndroidTestRule.activity.getString(R.string.home_title)

private val totalCoinsLabel: String
get() = composeAndroidTestRule.activity.getString(R.string.portfolio_card_total_coin_label)

private val todayProfitLabel: String
get() = composeAndroidTestRule.activity.getString(R.string.portfolio_card_today_profit_label)

private val errorGeneric: String
get() = composeAndroidTestRule.activity.getString(R.string.error_generic)

private val expectedPriceChange: String
get() = composeAndroidTestRule.activity.getString(
R.string.coin_profit_percent,
MockUtil.trendingCoins.first().priceChangePercentage24hInCurrency.toFormattedString()
)

private val mockGetMyCoinsUseCase = mockk<GetMyCoinsUseCase>()
private val mockGetTrendingCoinsUseCase = mockk<GetTrendingCoinsUseCase>()

private lateinit var viewModel: HomeViewModel

private var appDestination: AppDestination? = null

@Before
fun setUp() {
composeAndroidTestRule.activity.setContent {
HomeScreen(
viewModel = viewModel,
navigator = { destination -> appDestination = destination }
)
}

every { mockGetMyCoinsUseCase.execute(any()) } returns flowOf(MockUtil.myCoins)
every { mockGetTrendingCoinsUseCase.execute(any()) } returns flowOf(MockUtil.trendingCoins)
}

@Test
fun when_enter_to_HomeScreen_it_render_the_PortfolioCard_properly() {
initViewModel()

with(composeAndroidTestRule) {
onNodeWithTag(testTag = TestTagHomeTitle).assertTextEquals(homeTitle)
onNodeWithTag(testTag = TestTagTotalCoinsLabel).assertTextEquals(totalCoinsLabel)
onNodeWithTag(testTag = TestTagTodayCoinProfitLabel).assertTextEquals(todayProfitLabel)
onNodeWithTag(testTag = TestTagCardTotalCoins).assertTextEquals("$7,273,291")
onNodeWithTag(testTag = TestTagCardTodayProfit).assertTextEquals("$193,280")
}
}

@Test
fun when_enter_to_HomeScreen_and_load_MyCoins_successfully_it_render_the_UI_properly() {
initViewModel()

with(composeAndroidTestRule) {
with(MockUtil.myCoins.first()) {
onAllNodesWithTag(
testTag = TestTagCoinItemSymbol,
useUnmergedTree = true
).onFirst().assertTextEquals(symbol.uppercase())

onAllNodesWithTag(
testTag = TestTagCoinItemCoinName,
useUnmergedTree = true
).onFirst().assertTextEquals(coinName)

onAllNodesWithTag(
testTag = TestTagCoinItemPriceChange,
useUnmergedTree = true
).onFirst().onChild().assertTextEquals(expectedPriceChange)
}
}
}

@Test
fun when_enter_to_HomeScreen_and_load_TrendingCoins_successfully_it_render_the_UI_properly() {
initViewModel()

with(composeAndroidTestRule) {
with(MockUtil.trendingCoins.first()) {
onAllNodesWithTag(
testTag = TestTagTrendingItemSymbol,
useUnmergedTree = true
).onFirst().assertTextEquals(symbol.uppercase())

onAllNodesWithTag(
testTag = TestTagTrendingItemCoinName,
useUnmergedTree = true
).onFirst().assertTextEquals(coinName)

onAllNodesWithTag(
testTag = TestTagTrendingItemPriceChange,
useUnmergedTree = true
).onFirst().onChild().assertTextEquals(expectedPriceChange)
}
}
}

@Test
fun when_clicked_on_MyCoin_item_it_navigates_to_DetailScreen() {
initViewModel()

composeAndroidTestRule.onAllNodesWithTag(testTag = TestTagCoinItem).onFirst().performClick()

appDestination shouldBe AppDestination.CoinDetail
}

@Test
fun when_clicked_on_TrendingCoin_item_it_navigates_to_DetailScreen() {
initViewModel()

composeAndroidTestRule.onAllNodesWithTag(
testTag = TestTagTrendingItem
).onFirst().performClick()

appDestination shouldBe AppDestination.CoinDetail
}

@Test
fun when_enter_to_HomeScreen_and_load_MyCoins_failed_it_shows_the_Toast_properly() {
every { mockGetMyCoinsUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

initViewModel()

composeAndroidTestRule.onNodeWithTag(
testTag = TestTagCoinItem,
useUnmergedTree = true
).assertDoesNotExist()
}

@Test
fun when_enter_to_HomeScreen_and_load_TrendingCoins_failed_it_shows_the_Toast_properly() {
every { mockGetTrendingCoinsUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

initViewModel()

composeAndroidTestRule.onNodeWithTag(
testTag = TestTagTrendingItem,
useUnmergedTree = true
).assertDoesNotExist()
}

private fun initViewModel() {
viewModel = HomeViewModel(
dispatchers = testDispatcherProvider,
getMyCoinsUseCase = mockGetMyCoinsUseCase,
getTrendingCoinsUseCase = mockGetTrendingCoinsUseCase
)
}
}
Loading

0 comments on commit 5d95fc7

Please sign in to comment.