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 1 commit
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
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)
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved

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

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

with(composeAndroidTestRule) {
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved
with(MockUtil.myCoins.first()) {
onAllNodesWithTag(
testTag = TestTagCoinItemSymbol,
useUnmergedTree = true
).onFirst().assertTextEquals(symbol.uppercase())
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved

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() {
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved
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() {
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved
every { mockGetMyCoinsUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

initViewModel()

composeAndroidTestRule.onNodeWithTag(
testTag = TestTagCoinItem,
useUnmergedTree = true
).assertDoesNotExist()
}
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved

@Test
fun when_enter_to_HomeScreen_and_load_TrendingCoins_failed_it_shows_the_Toast_properly() {
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved
every { mockGetTrendingCoinsUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

initViewModel()

composeAndroidTestRule.onNodeWithTag(
testTag = TestTagTrendingItem,
useUnmergedTree = true
).assertDoesNotExist()
}
Wadeewee marked this conversation as resolved.
Show resolved Hide resolved

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