Skip to content

Commit

Permalink
Merge pull request #96 from nimblehq/chore/70-create-detail-screen-un…
Browse files Browse the repository at this point in the history
…it-test

[#70] Add UnitTest for DetailScreen
  • Loading branch information
hoangnguyen92dn authored Feb 21, 2023
2 parents 7326762 + 63c027a commit 6931661
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
Expand Down Expand Up @@ -41,6 +42,13 @@ import me.bytebeats.views.charts.line.render.line.SolidLineDrawer
import me.bytebeats.views.charts.line.render.point.EmptyPointDrawer
import me.bytebeats.views.charts.simpleChartAnimation

const val TestTagDetailLogo = "DetailLogo"
const val TestTagDetailCircularProgress = "DetailCircularProgress"
const val TestTagDetailLineChart = "DetailLineChart"
const val TestTagDetailChartInterval = "DetailChartInterval"
const val TestTagDetailCoinInfo = "DetailCoinInfo"
const val TestTagDetailSellBuyGroup = "DetailSellBuyGroup"

@Composable
fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel(),
Expand Down Expand Up @@ -127,7 +135,8 @@ private fun DetailScreenContent(
top.linkTo(appBar.bottom)
linkTo(start = parent.start, end = parent.end)
}
.padding(top = Dp8),
.padding(top = Dp8)
.testTag(tag = TestTagDetailLogo),
painter = rememberAsyncImagePainter(coinDetailUiModel.image),
contentDescription = null
)
Expand Down Expand Up @@ -164,7 +173,8 @@ private fun DetailScreenContent(
top.linkTo(priceChangePercent.bottom, margin = Dp24)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
}
.testTag(tag = TestTagDetailLineChart),
lineChartData = LineChartData(
points = coinPrices.map { coinPrice ->
val price = stringResource(
Expand All @@ -189,11 +199,13 @@ private fun DetailScreenContent(

// Chart intervals
ChartIntervalsButtonGroup(
modifier = Modifier.constrainAs(intervals) {
top.linkTo(graph.bottom, margin = Dp24)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
modifier = Modifier
.constrainAs(intervals) {
top.linkTo(graph.bottom, margin = Dp24)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.testTag(tag = TestTagDetailChartInterval),
onIntervalChanged = onTimeIntervalsChanged::invoke
)

Expand All @@ -216,7 +228,8 @@ private fun DetailScreenContent(
top = parent.top,
bottom = parent.bottom
)
},
}
.testTag(tag = TestTagDetailCircularProgress),
)
}
}
Expand All @@ -225,7 +238,8 @@ private fun DetailScreenContent(
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
.navigationBarsPadding()
.testTag(tag = TestTagDetailSellBuyGroup),
contentAlignment = Alignment.BottomEnd
) {
SellBuyGroup(
Expand All @@ -244,7 +258,11 @@ private fun CoinInfo(
sellBuyLayoutHeight: Dp,
coinDetailUiModel: CoinDetailUiModel
) {
Column(modifier = modifier.padding(start = Dp16, end = Dp16, bottom = sellBuyLayoutHeight)) {
Column(
modifier = modifier
.padding(start = Dp16, end = Dp16, bottom = sellBuyLayoutHeight)
.testTag(tag = TestTagDetailCoinInfo)
) {
DetailItem(
modifier = Modifier,
title = stringResource(id = R.string.detail_market_cap_title),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package co.nimblehq.compose.crypto.ui.screens.detail

import androidx.activity.compose.setContent
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.*
import androidx.navigation.*
import co.nimblehq.compose.crypto.R
import co.nimblehq.compose.crypto.domain.usecase.GetCoinDetailUseCase
import co.nimblehq.compose.crypto.domain.usecase.GetCoinPricesUseCase
import co.nimblehq.compose.crypto.extension.toFormattedString
import co.nimblehq.compose.crypto.test.MockUtil
import co.nimblehq.compose.crypto.ui.components.chartintervals.TimeIntervals
import co.nimblehq.compose.crypto.ui.navigation.AppDestination
import co.nimblehq.compose.crypto.ui.screens.BaseViewModelTest
import co.nimblehq.compose.crypto.ui.screens.MainActivity
import co.nimblehq.compose.crypto.ui.uimodel.toUiModel
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowToast
import kotlin.math.abs

@RunWith(RobolectricTestRunner::class)
@ExperimentalCoroutinesApi
class DetailScreenTest : BaseViewModelTest() {

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

private val marketCapTitle: String
get() = composeAndroidTestRule.activity.getString(R.string.detail_market_cap_title)

private val allTimeHighTitle: String
get() = composeAndroidTestRule.activity.getString(R.string.detail_all_time_high_title)

private val allTimeLowTitle: String
get() = composeAndroidTestRule.activity.getString(R.string.detail_all_time_low_title)

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

private val mockGetCoinDetailUseCase = mockk<GetCoinDetailUseCase>()
private val mockGetCoinPricesUseCase = mockk<GetCoinPricesUseCase>()

private lateinit var viewModel: DetailViewModel

private var appDestination: AppDestination? = null

private val coinDetailUiModel = MockUtil.coinDetail.toUiModel()

@Before
fun setUp() {
every { mockGetCoinDetailUseCase.execute(any()) } returns flowOf(MockUtil.coinDetail)
every { mockGetCoinPricesUseCase.execute(any()) } returns flowOf(emptyList())

initViewModel()

composeAndroidTestRule.activity.setContent {
DetailScreen(
viewModel = viewModel,
navigator = { destination -> appDestination = destination },
coinId = "Bitcoin"
)
}
}

@Test
fun `When entering to the DetailScreen, it shows the loading properly`() {
every { mockGetCoinDetailUseCase.execute(any()) } returns flow { delay(100) }

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

@Test
fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the HeaderDetail properly`() {
with(composeAndroidTestRule) {
onNodeWithTag(testTag = TestTagDetailLogo).assertIsDisplayed()

onNodeWithText(coinDetailUiModel.coinName).assertIsDisplayed()
onNodeWithText(
"$${coinDetailUiModel.currentPrice.toFormattedString()}"
).assertIsDisplayed()
onNodeWithText(
"${abs(coinDetailUiModel.priceChangePercentage24hInCurrency).toFormattedString()}%"
).assertIsDisplayed()
}
}

@Test
fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the Chart properly`() {
with(composeAndroidTestRule) {
onNodeWithTag(testTag = TestTagDetailLineChart).assertExists()
onNodeWithTag(testTag = TestTagDetailChartInterval).assertExists()

onNodeWithText(TimeIntervals.ONE_DAY.text).assertIsDisplayed().assertHasClickAction()
onNodeWithText(TimeIntervals.ONE_WEEK.text).assertIsDisplayed().assertHasClickAction()
onNodeWithText(TimeIntervals.ONE_MONTH.text).assertIsDisplayed().assertHasClickAction()
onNodeWithText(TimeIntervals.ONE_YEAR.text).assertIsDisplayed().assertHasClickAction()
onNodeWithText(TimeIntervals.FIVE_YEAR.text).assertIsDisplayed().assertHasClickAction()
}
}

@Test
fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the CoinInfo properly`() {
with(composeAndroidTestRule) {
onNodeWithTag(testTag = TestTagDetailCoinInfo).assertExists()

onNodeWithText(marketCapTitle).assertIsDisplayed()
onNodeWithText(allTimeHighTitle).assertIsDisplayed()
onNodeWithText(allTimeLowTitle).assertIsDisplayed()

onNodeWithText("$${coinDetailUiModel.marketCap.toFormattedString()}").assertIsDisplayed()
onNodeWithText("$${coinDetailUiModel.ath.toFormattedString()}").assertIsDisplayed()
onNodeWithText("$${coinDetailUiModel.atl.toFormattedString()}").assertIsDisplayed()

onNodeWithText(
"${abs(coinDetailUiModel.marketCapChangePercentage24h).toFormattedString()}%"
).assertIsDisplayed()
onNodeWithText(
"${abs(coinDetailUiModel.athChangePercentage).toFormattedString()}%"
).assertIsDisplayed()
onNodeWithText(
"${abs(coinDetailUiModel.atlChangePercentage).toFormattedString()}%"
).assertIsDisplayed()
}
}

@Test
fun `When entering to the DetailScreen and GetCoinDetail successfully, it renders the SellBuyGroup properly`() {
composeAndroidTestRule.onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertExists()
}

@Test
fun `When entering to the DetailScreen and GetCoinDetail failed, it shows the error message properly`() {
every { mockGetCoinDetailUseCase.execute(any()) } returns flow {
throw Throwable(errorGeneric)
}

with(composeAndroidTestRule) {
onNodeWithText(coinDetailUiModel.coinName).assertDoesNotExist()
onNodeWithText(
"$${coinDetailUiModel.currentPrice.toFormattedString()}"
).assertDoesNotExist()
onNodeWithText(
"${abs(coinDetailUiModel.priceChangePercentage24hInCurrency).toFormattedString()}%"
).assertDoesNotExist()

onNodeWithTag(testTag = TestTagDetailLogo).assertDoesNotExist()
onNodeWithTag(testTag = TestTagDetailLineChart).assertDoesNotExist()
onNodeWithTag(testTag = TestTagDetailChartInterval).assertDoesNotExist()
onNodeWithTag(testTag = TestTagDetailCoinInfo).assertDoesNotExist()
onNodeWithTag(testTag = TestTagDetailSellBuyGroup).assertDoesNotExist()
}

ShadowToast.getTextOfLatestToast() shouldBe errorGeneric
}

private fun initViewModel() {
viewModel = DetailViewModel(
dispatchers = testDispatcherProvider,
getCoinDetailUseCase = mockGetCoinDetailUseCase,
getCoinPricesUseCase = mockGetCoinPricesUseCase
)
}
}

0 comments on commit 6931661

Please sign in to comment.