Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#74] Create HomeScreenUITest #86

Merged
merged 7 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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.test

import co.nimblehq.compose.crypto.util.DispatchersProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher

@OptIn(ExperimentalCoroutinesApi::class)
object TestDispatchersProvider : DispatchersProvider {

private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()

override val io: CoroutineDispatcher
get() = testDispatcher

override val main: CoroutineDispatcher
get() = testDispatcher

override val default: CoroutineDispatcher
get() = testDispatcher
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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.test.TestDispatchersProvider
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.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeScreenUITest {

@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_entering_HomeScreen__it_renders_the_PortfolioCard_properly() {
initViewModel()
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved

with(composeAndroidTestRule) {
onNodeWithText(homeTitle).assertIsDisplayed()
onNodeWithText(totalCoinsLabel).assertIsDisplayed()
onNodeWithText(todayProfitLabel).assertIsDisplayed()
onNodeWithText("$7,273,291").assertIsDisplayed()
onNodeWithText("$193,280").assertIsDisplayed()
}
}

@Test
fun when_loading_MyCoins__it_renders_the_LoadingProgress_properly() {
every { mockGetMyCoinsUseCase.execute(any()) } returns flow { delay(500) }

initViewModel()

composeAndroidTestRule.onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed()
}

@Test
fun when_loading_TrendingCoins__it_renders_the_LoadingProgress_properly() {
every { mockGetTrendingCoinsUseCase.execute(any()) } returns flow { delay(500) }

initViewModel()

composeAndroidTestRule.onNodeWithTag(testTag = TestTagCoinsLoader).assertIsDisplayed()
}

@Test
fun when_entering_HomeScreen_and_loading_MyCoins_successfully__it_renders_the_UI_properly() {
initViewModel()

with(composeAndroidTestRule) {
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved
with(MockUtil.myCoins.first()) {
onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed()
onAllNodesWithText(coinName).onFirst().assertIsDisplayed()
onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed()
}
}
}

@Test
fun when_entering_to_the_HomeScreen_and_loading_TrendingCoins_successfully__it_renders_the_UI_properly() {
initViewModel()

with(composeAndroidTestRule) {
with(MockUtil.trendingCoins.first()) {
onAllNodesWithText(symbol.uppercase()).onFirst().assertIsDisplayed()
onAllNodesWithText(coinName).onFirst().assertIsDisplayed()
onAllNodesWithText(expectedPriceChange).onFirst().assertIsDisplayed()
}
}
}

@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_entering_to_the_HomeScreen_and_loading_MyCoins_failed__it_shows_the_error_message() {
every { mockGetMyCoinsUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

initViewModel()

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

// TODO: Add the assertion for the error message
}

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

initViewModel()

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

// TODO: Add the assertion for the error message
}

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