Skip to content

Commit

Permalink
[#69] Create HomeScreen UnitTest
Browse files Browse the repository at this point in the history
  • Loading branch information
Wadeewee committed Jan 11, 2023
1 parent 5c0de22 commit 0d5f638
Show file tree
Hide file tree
Showing 11 changed files with 418 additions and 37 deletions.
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ android {
xmlReport = true
xmlOutput = file("build/reports/lint/lint-result.xml")
}

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

kapt {
Expand Down Expand Up @@ -145,6 +151,7 @@ dependencies {
kapt("com.google.dagger:hilt-compiler:${Versions.HILT_VERSION}")

debugImplementation("androidx.compose.ui:ui-tooling:${Versions.COMPOSE_VERSION}")
debugImplementation ("androidx.compose.ui:ui-test-manifest:${Versions.COMPOSE_VERSION}")

// Testing
testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}")
Expand All @@ -158,6 +165,9 @@ 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 ("org.robolectric:shadows-httpclient:${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
Expand Up @@ -17,13 +17,17 @@ fun AppNavigation(
) {
composable(AppDestination.Home) {
HomeScreen(
navigator = { destination -> navController.navigate(destination) }
navigator = { destination ->
navController.navigate(destination)
}
)
}

composable(AppDestination.CoinDetail) {
DetailScreen(
navigator = { destination -> navController.navigate(destination) },
navigator = { destination ->
navController.navigate(destination)
},
coinId = it.arguments?.getString(KEY_COIN_ID).orEmpty()
)
}
Expand All @@ -46,6 +50,8 @@ private fun NavGraphBuilder.composable(
private fun NavHostController.navigate(destination: AppDestination) {
when (destination) {
is AppDestination.Up -> popBackStack()
else -> navigate(route = destination.destination)
else -> {
navigate(route = destination.destination)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ fun DetailItem(
}
}

@Composable
@Preview
@Composable
fun DetailItemPreview() {
ComposeTheme {
Surface {
Expand All @@ -85,8 +85,8 @@ fun DetailItemPreview() {
}
}

@Composable
@Preview
@Composable
fun DetailItemPreviewDark() {
ComposeTheme(darkTheme = true) {
Surface {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 All @@ -38,13 +39,19 @@ import co.nimblehq.compose.crypto.ui.theme.Style.textColor
import co.nimblehq.compose.crypto.ui.uimodel.CoinItemUiModel
import coil.compose.rememberAsyncImagePainter

const val TestTagCoinItemSymbol = "CoinItemCoinSymbol"
const val TestTagCoinItemCoinName = "CoinItemCoinName"
const val TestTagCoinItemPrice = "CoinItemPrice"
const val TestTagCoinItemPriceChange = "CoinItemPriceChange"

@Composable
fun CoinItem(
modifier: Modifier,
coinItem: CoinItemUiModel,
onItemClick: () -> Unit
) {
ConstraintLayout(
modifier = Modifier
modifier = modifier
.wrapContentWidth()
.clip(RoundedCornerShape(Dp12))
.clickable { onItemClick.invoke() }
Expand Down Expand Up @@ -76,7 +83,8 @@ fun CoinItem(
.constrainAs(coinSymbol) {
top.linkTo(parent.top)
start.linkTo(anchor = logo.end, margin = Dp16)
},
}
.testTag(tag = TestTagCoinItemSymbol),
text = coinItem.symbol.uppercase(),
color = MaterialTheme.colors.textColor,
style = Style.semiBold16()
Expand All @@ -89,7 +97,8 @@ fun CoinItem(
start.linkTo(coinSymbol.start)
top.linkTo(coinSymbol.bottom)
width = Dimension.preferredWrapContent
},
}
.testTag(tag = TestTagCoinItemCoinName),
text = coinItem.coinName,
color = MaterialTheme.colors.coinNameColor,
style = Style.medium14()
Expand All @@ -101,7 +110,8 @@ fun CoinItem(
start.linkTo(logo.start)
top.linkTo(anchor = coinName.bottom, margin = Dp14)
width = Dimension.preferredWrapContent
},
}
.testTag(tag = TestTagCoinItemPrice),
text = stringResource(
R.string.coin_currency,
coinItem.currentPrice.toFormattedString()
Expand All @@ -119,6 +129,7 @@ fun CoinItem(
bottom.linkTo(parent.bottom)
width = Dimension.preferredWrapContent
}
.testTag(tag = TestTagCoinItemPriceChange)
)
}
}
Expand All @@ -130,6 +141,7 @@ fun CoinItemPreview(
) {
ComposeTheme {
CoinItem(
modifier = Modifier,
coinItem = coinItem,
onItemClick = {}
)
Expand All @@ -143,6 +155,7 @@ fun CoinItemPreviewDark(
) {
ComposeTheme {
CoinItem(
modifier = Modifier,
coinItem = coinItem,
onItemClick = {}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
Expand Down Expand Up @@ -42,21 +43,22 @@ import timber.log.Timber

private const val LIST_ITEM_LOAD_MORE_THRESHOLD = 0

const val TestTagTrendingItem = "TrendingItem"
const val TestTagCoinItem = "CoinItem"
const val TestTagCoinsLoader = "CoinsLoader"

@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
navigator: (destination: AppDestination) -> Unit
) {
val context = LocalContext.current
var rememberRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.output.error.collect { error ->
val message = error.userReadableMessage(context)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}

LaunchedEffect(viewModel) {
viewModel.output.navigator.collect { destination -> navigator(destination) }
viewModel.output.navigator.collect { destination ->
navigator(destination)
}
}
LaunchedEffect(viewModel.showLoading) {
viewModel.showLoading.collect { isRefreshing ->
Expand All @@ -68,6 +70,16 @@ fun HomeScreen(
val showTrendingCoinsLoading: LoadingState by viewModel.output.showTrendingCoinsLoading.collectAsState()
val myCoins: List<CoinItemUiModel> by viewModel.output.myCoins.collectAsState()
val trendingCoins: List<CoinItemUiModel> by viewModel.output.trendingCoins.collectAsState()
val myCoinsError: Throwable? by viewModel.output.myCoinsError.collectAsState()
val trendingCoinsError: Throwable? by viewModel.output.trendingCoinsError.collectAsState()

myCoinsError?.let { error ->
Toast.makeText(context, error.userReadableMessage(context), Toast.LENGTH_SHORT).show()
}

trendingCoinsError?.let { error ->
Toast.makeText(context, error.userReadableMessage(context), Toast.LENGTH_SHORT).show()
}

HomeScreenContent(
showMyCoinsLoading = showMyCoinsLoading,
Expand Down Expand Up @@ -117,7 +129,8 @@ private fun HomeScreenContent(
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = Dp16),
.padding(top = Dp16)
.testTag(tag = stringResource(id = R.string.home_title)),
text = stringResource(id = R.string.home_title),
textAlign = TextAlign.Center,
style = Style.semiBold24(),
Expand Down Expand Up @@ -173,7 +186,8 @@ private fun HomeScreenContent(
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally),
.wrapContentWidth(align = Alignment.CenterHorizontally)
.testTag(tag = TestTagCoinsLoader),
)
}
} else {
Expand All @@ -191,6 +205,7 @@ private fun HomeScreenContent(
)
) {
TrendingItem(
modifier = Modifier.testTag(tag = TestTagTrendingItem),
coinItem = coin,
onItemClick = { onTrendingItemClick.invoke(coin) }
)
Expand All @@ -206,6 +221,7 @@ private fun HomeScreenContent(
.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally)
.padding(bottom = Dp16)
.testTag(tag = TestTagCoinsLoader),
)
}
}
Expand Down Expand Up @@ -269,7 +285,8 @@ private fun MyCoins(
.constrainAs(myCoins) {
top.linkTo(myCoinsTitle.bottom, margin = Dp16)
linkTo(start = parent.start, end = parent.end)
},
}
.testTag(tag = TestTagCoinsLoader),
)
} else {
LazyRow(
Expand All @@ -283,6 +300,7 @@ private fun MyCoins(
) {
items(coins) { coin ->
CoinItem(
modifier = Modifier.testTag(tag = TestTagCoinItem),
coinItem = coin,
onItemClick = { onMyCoinsItemClick.invoke(coin) }
)
Expand All @@ -292,9 +310,9 @@ private fun MyCoins(
}
}

@Composable
@Preview(showSystemUi = true, uiMode = UI_MODE_NIGHT_NO)
fun HomeScreenPreview(
@Composable
private fun HomeScreenPreview(
@PreviewParameter(HomeScreenPreviewParameterProvider::class) params: HomeScreenParams
) {
with(params) {
Expand All @@ -310,9 +328,9 @@ fun HomeScreenPreview(
}
}

@Composable
@Preview(showSystemUi = true, uiMode = UI_MODE_NIGHT_YES)
fun HomeScreenPreviewDark(
@Composable
private fun HomeScreenPreviewDark(
@PreviewParameter(HomeScreenPreviewParameterProvider::class) params: HomeScreenParams
) {
with(params) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ interface Output : BaseOutput {
val myCoins: StateFlow<List<CoinItemUiModel>>

val trendingCoins: StateFlow<List<CoinItemUiModel>>

val myCoinsError: SharedFlow<Throwable?>

val trendingCoinsError: SharedFlow<Throwable?>
}

@HiltViewModel
Expand Down Expand Up @@ -66,6 +70,14 @@ class HomeViewModel @Inject constructor(
override val trendingCoins: StateFlow<List<CoinItemUiModel>>
get() = _trendingCoins

private val _myCoinsError = MutableStateFlow<Throwable?>(null)
override val myCoinsError: StateFlow<Throwable?>
get() = _myCoinsError

private val _trendingCoinsError = MutableStateFlow<Throwable?>(null)
override val trendingCoinsError: StateFlow<Throwable?>
get() = _trendingCoinsError

private var trendingCoinsPage = MY_COINS_INITIAL_PAGE

init {
Expand All @@ -81,7 +93,11 @@ class HomeViewModel @Inject constructor(
}

private fun getMyCoins(isRefreshing: Boolean) = execute {
if (isRefreshing) showLoading() else _showMyCoinsLoading.value = true
if (isRefreshing) {
showLoading()
} else {
_showMyCoinsLoading.value = true
}
getMyCoinsUseCase.execute(
GetMyCoinsUseCase.Input(
currency = FIAT_CURRENCY,
Expand All @@ -92,12 +108,16 @@ class HomeViewModel @Inject constructor(
)
)
.catch { e ->
_error.emit(e)
_myCoinsError.emit(e)
}
.collect { coins ->
_myCoins.emit(coins.map { it.toUiModel() })
}
if (isRefreshing) hideLoading() else _showMyCoinsLoading.value = false
if (isRefreshing) {
hideLoading()
} else {
_showMyCoinsLoading.value = false
}
}

override fun getTrendingCoins(isRefreshing: Boolean, loadMore: Boolean) {
Expand All @@ -115,7 +135,7 @@ class HomeViewModel @Inject constructor(
)
)
.catch { e ->
_error.emit(e)
_trendingCoinsError.emit(e)
}
.collect { coins ->
val newCoinList = coins.map { it.toUiModel() }
Expand All @@ -126,8 +146,11 @@ class HomeViewModel @Inject constructor(
}
trendingCoinsPage++
}
if (isRefreshing) hideLoading() else
if (isRefreshing) {
hideLoading()
} else {
_showTrendingCoinsLoading.value = LoadingState.Idle
}
}
}

Expand Down
Loading

0 comments on commit 0d5f638

Please sign in to comment.