Skip to content

Commit

Permalink
Merge pull request #80 from odaridavid/instrumented-robolectric
Browse files Browse the repository at this point in the history
Add instrumentation tests
  • Loading branch information
odaridavid authored Mar 18, 2024
2 parents 6972097 + ccb898e commit b7024fa
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ android {
// Breaks jacoco reporting if true see https://github.com/firebase/firebase-android-sdk/issues/3948
setInstrumentationEnabled(false)
}
enableAndroidTestCoverage = true
}

release {
Expand Down Expand Up @@ -208,6 +209,10 @@ dependencies {
testImplementation(libs.coroutines.test)
debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
// Android Test
androidTestImplementation(libs.bundles.android.test)
androidTestImplementation(libs.coroutines.test)
androidTestImplementation(libs.turbine)

// Chucker
debugImplementation(libs.chucker.debug)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.github.odaridavid.weatherapp

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import app.cash.turbine.test
import com.github.odaridavid.weatherapp.core.api.SettingsRepository
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.ExcludedData
import com.github.odaridavid.weatherapp.core.model.SupportedLanguage
import com.github.odaridavid.weatherapp.core.model.TimeFormat
import com.github.odaridavid.weatherapp.core.model.Units
import com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test

@SmallTest
class SettingsRepositoryTest {

// TODO instrumentation test coverage
private lateinit var settingsRepository: SettingsRepository

@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
settingsRepository = DefaultSettingsRepository(context)
}

@Test
fun when_we_update_language_then_we_get_the_updated_language() = runTest {
settingsRepository.setLanguage(SupportedLanguage.FRENCH)
val language = settingsRepository.getLanguage()
language.test {
awaitItem().also { language ->
assert(language == SupportedLanguage.FRENCH)
}
}
}

@Test
fun when_we_update_units_then_we_get_the_updated_units() = runTest {
settingsRepository.setUnits(Units.IMPERIAL)
val units = settingsRepository.getUnits()
units.test {
awaitItem().also { units ->
assert(units == Units.IMPERIAL)
}
}
}

@Test
fun when_we_update_time_format_then_we_get_the_updated_time_format() = runTest {
settingsRepository.setFormat(TimeFormat.TWENTY_FOUR_HOUR)
val timeFormat = settingsRepository.getFormat()
timeFormat.test {
awaitItem().also { timeFormat ->
assert(timeFormat == TimeFormat.TWENTY_FOUR_HOUR)
}
}
}

@Test
fun when_we_update_excluded_data_then_we_get_the_updated_excluded_data() = runTest {
val excludedData = listOf(ExcludedData.MINUTELY, ExcludedData.ALERTS)
settingsRepository.setExcludedData(excludedData)
val data = settingsRepository.getExcludedData()
data.test {
awaitItem().also { data ->
assert(data == "minutely,alerts")
}
}
}

@Test
fun when_we_update_default_location_then_we_get_the_updated_default_location() = runTest {
val defaultLocation = DefaultLocation(23.23, 34.12)
settingsRepository.setDefaultLocation(defaultLocation)
val location = settingsRepository.getDefaultLocation()
location.test {
awaitItem().also { location ->
assert(location == defaultLocation)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ class HomeViewModel @Inject constructor(
units = units,
defaultLocation = defaultLocation
)
}
processIntent(HomeScreenIntent.LoadWeatherData)
}.also { processIntent(HomeScreenIntent.LoadWeatherData) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package com.github.odaridavid.weatherapp

import app.cash.turbine.test
import com.github.odaridavid.weatherapp.core.Result
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.SettingsRepository
import com.github.odaridavid.weatherapp.core.api.WeatherRepository
import com.github.odaridavid.weatherapp.core.model.DefaultLocation
import com.github.odaridavid.weatherapp.core.model.ExcludedData
import com.github.odaridavid.weatherapp.core.model.SupportedLanguage
import com.github.odaridavid.weatherapp.core.model.TimeFormat
import com.github.odaridavid.weatherapp.core.model.Units
import com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository
import com.github.odaridavid.weatherapp.data.weather.remote.DefaultRemoteWeatherDataSource
import com.github.odaridavid.weatherapp.data.weather.remote.OpenWeatherService
import com.github.odaridavid.weatherapp.data.weather.remote.WeatherResponse
import com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository
import com.github.odaridavid.weatherapp.fakes.fakeSuccessMappedWeatherResponse
import com.github.odaridavid.weatherapp.fakes.fakeSuccessWeatherResponse
import com.github.odaridavid.weatherapp.rules.MainCoroutineRule
Expand All @@ -27,7 +26,6 @@ import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Before
import org.junit.Rule
Expand All @@ -36,18 +34,16 @@ import retrofit2.Response
import java.util.TimeZone

@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelIntegrationTest {
class HomeViewModelTest {

@MockK
val mockOpenWeatherService = mockk<OpenWeatherService>(relaxed = true)

@MockK
val mockLogger = mockk<Logger>(relaxed = true)

@MockK
val mockSettingsRepository = mockk<SettingsRepository>(relaxed = true).apply {
coEvery { getFormat() } returns flowOf(TimeFormat.TWELVE_HOUR)
coEvery { getExcludedData() } returns flowOf(ExcludedData.NONE.value)
private val settingsRepository: SettingsRepository by lazy {
FakeSettingsRepository()
}

@get:Rule
Expand All @@ -68,6 +64,8 @@ class HomeViewModelIntegrationTest {
fakeSuccessWeatherResponse
)

settingsRepository.setFormat(TimeFormat.TWELVE_HOUR)

val weatherRepository = createWeatherRepository()

val viewModel = createViewModel(
Expand Down Expand Up @@ -133,13 +131,28 @@ class HomeViewModelIntegrationTest {

@Test
fun `when we init the screen, then update the state`() = runBlocking {
val weatherRepository = mockk<WeatherRepository>() {
coEvery { fetchWeatherData(any(), any(), any()) } returns Result.Success(
fakeSuccessMappedWeatherResponse
coEvery {
mockOpenWeatherService.getWeatherData(
any(), any(), any(), any(), any(), any()
)
} returns Response.success<WeatherResponse>(
fakeSuccessWeatherResponse
)

val settingsRepository = mockk<SettingsRepository>() {
coEvery { getDefaultLocation() } returns flowOf(DefaultLocation(0.0, 0.0))
coEvery { getLanguage() } returns flowOf(SupportedLanguage.ENGLISH)
coEvery { getUnits() } returns flowOf(Units.METRIC)
coEvery { getFormat() } returns flowOf(TimeFormat.TWELVE_HOUR)
coEvery { getExcludedData() } returns flowOf("minutely,alerts")
}

val viewModel = createViewModel(weatherRepository = weatherRepository)
val viewModel = createViewModel(
settingsRepository = settingsRepository,
weatherRepository = createWeatherRepository(
settingsRepository = settingsRepository
)
)

val expectedState = HomeScreenViewState(
units = Units.METRIC,
Expand All @@ -161,17 +174,31 @@ class HomeViewModelIntegrationTest {
}

@Test
fun `when we receive a city name, the state is updated with it`() = runTest {
val weatherRepository = mockk<WeatherRepository>() {
coEvery { fetchWeatherData(any(), any(), any()) } returns Result.Success(
fakeSuccessMappedWeatherResponse
fun `when we receive a city name, the state is updated with it`() = runBlocking {
coEvery {
mockOpenWeatherService.getWeatherData(
any(), any(), any(), any(), any(), any()
)
} returns Response.success<WeatherResponse>(
fakeSuccessWeatherResponse
)
val settingsRepository = mockk<SettingsRepository>() {
coEvery { getDefaultLocation() } returns flowOf(DefaultLocation(0.0, 0.0))
coEvery { getLanguage() } returns flowOf(SupportedLanguage.ENGLISH)
coEvery { getUnits() } returns flowOf(Units.METRIC)
coEvery { getFormat() } returns flowOf(TimeFormat.TWELVE_HOUR)
coEvery { getExcludedData() } returns flowOf("minutely,alerts")
}

val viewModel = createViewModel(
weatherRepository = weatherRepository
settingsRepository = settingsRepository,
weatherRepository = createWeatherRepository(
settingsRepository = settingsRepository
)
)

viewModel.processIntent(HomeScreenIntent.DisplayCityName(cityName = "Paradise"))

val expectedState = HomeScreenViewState(
units = Units.METRIC,
defaultLocation = DefaultLocation(
Expand All @@ -184,8 +211,6 @@ class HomeViewModelIntegrationTest {
errorMessageId = null
)

viewModel.processIntent(HomeScreenIntent.DisplayCityName(cityName = "Paradise"))

viewModel.state.test {
awaitItem().also { state ->
Truth.assertThat(state).isEqualTo(expectedState)
Expand All @@ -195,29 +220,20 @@ class HomeViewModelIntegrationTest {

private fun createViewModel(
weatherRepository: WeatherRepository,
settingsRepository: SettingsRepository = mockk<SettingsRepository>() {
coEvery { getUnits() } returns flowOf(Units.METRIC)
coEvery { getDefaultLocation() } returns flowOf(
DefaultLocation(
longitude = 0.0, latitude = 0.0
)
)
coEvery { getLanguage() } returns flowOf(SupportedLanguage.ENGLISH)
coEvery { getAppVersion() } returns "1.0.0"
coEvery { getFormat() } returns flowOf(TimeFormat.TWENTY_FOUR_HOUR)
coEvery { getExcludedData() } returns flowOf(ExcludedData.CURRENT.value)
}
settingsRepository: SettingsRepository = this.settingsRepository
): HomeViewModel =
HomeViewModel(
weatherRepository = weatherRepository,
settingsRepository = settingsRepository
)

private fun createWeatherRepository() = DefaultWeatherRepository(
private fun createWeatherRepository(
settingsRepository: SettingsRepository = this.settingsRepository
) = DefaultWeatherRepository(
remoteWeatherDataSource = DefaultRemoteWeatherDataSource(
mockOpenWeatherService
),
logger = mockLogger,
settingsRepository = mockSettingsRepository,
settingsRepository = settingsRepository,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.github.odaridavid.weatherapp
import app.cash.turbine.test
import com.github.odaridavid.weatherapp.core.api.Logger
import com.github.odaridavid.weatherapp.core.api.SettingsRepository
import com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository
import com.github.odaridavid.weatherapp.rules.MainCoroutineRule
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
Expand All @@ -14,7 +14,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test

class MainViewModelIntegrationTest {
class MainViewModelTest {

@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
Expand All @@ -25,9 +25,8 @@ class MainViewModelIntegrationTest {
every { logException(any()) } returns Unit
}

@MockK
val settingsRepository = mockk<SettingsRepository>().apply {
coEvery { setDefaultLocation(any()) } returns Unit
private val settingsRepository: SettingsRepository by lazy {
FakeSettingsRepository()
}

@Test
Expand Down Expand Up @@ -98,7 +97,7 @@ class MainViewModelIntegrationTest {
}

private fun createMainViewModel(
settingsRepository: SettingsRepository = mockk(),
settingsRepository: SettingsRepository = this.settingsRepository,
logger: Logger = mockk(),
): MainViewModel {
return MainViewModel(settingsRepository = settingsRepository, logger = logger)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import org.junit.Test

class SettingsRepositoryUnitTest {
// TODO Integration test with the real data store
class SettingsRepositoryTest {

@Test
fun `when we update language, then we get the updated language`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class SettingsViewModelIntegrationTest {
class SettingsViewModelTest {

private val settingsRepository: SettingsRepository = FakeSettingsRepository()

Expand Down
Loading

0 comments on commit b7024fa

Please sign in to comment.