diff --git a/.idea/detekt.xml b/.idea/detekt.xml index f69c092..0623675 100644 --- a/.idea/detekt.xml +++ b/.idea/detekt.xml @@ -1,7 +1,6 @@ - true D:\AndroidLab\AndroidLab2\config\detekt\detekt.yml \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 526b4c2..a2d7c21 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ - diff --git a/app/build.gradle b/app/build.gradle index 15ac8f7..2a6a6fb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,6 +81,7 @@ dependencies { def coroutines = "1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" // region Network def retrofit = "2.9.0" @@ -104,8 +105,13 @@ dependencies { implementation "it.czerwinski.android.hilt:hilt-extensions:${hiltExt}" kapt "it.czerwinski.android.hilt:hilt-processor:${hiltExt}" - testImplementation 'junit:junit:4.13.2' + testImplementation 'io.mockk:mockk:1.12.3' + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.8.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.8.2" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.8.2" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit5:1.6.21" + androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModel.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModel.kt index 4e85725..d9d12de 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModel.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModel.kt @@ -18,23 +18,6 @@ class WeatherViewModel @Inject constructor( private val getWeatherUseCase: GetWeatherUseCase ) : ViewModel() { -// @AssistedFactory -// interface Factory { -// fun create(@Assisted cityId: Int): Factory -// } -// -// @Suppress("UNCHECKED_CAST") -// companion object { -// fun provideFactory( -// assistedFactory: Factory, -// cityId: Int -// ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { -// override fun create(modelClass: Class): T { -// return assistedFactory.create(cityId) as T -// } -// } -// } - private var _weather: MutableLiveData> = MutableLiveData() val weather: LiveData> get() = _weather @@ -50,7 +33,3 @@ class WeatherViewModel @Inject constructor( } } } - -//@Module -//@InstallIn(ActivityRetainedComponent::class) -//interface AssistedInjectModule diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherListUseCaseTest.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherListUseCaseTest.kt new file mode 100644 index 0000000..9a61564 --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherListUseCaseTest.kt @@ -0,0 +1,49 @@ +package ru.itis.karakurik.androidLab2.domain.usecase + +import io.mockk.* +import ru.itis.karakurik.androidLab2.utils.MainCoroutineRule +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import ru.itis.karakurik.androidLab2.domain.entity.Weather +import ru.itis.karakurik.androidLab2.domain.repository.WeatherRepository + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(JUnit4::class) +internal class GetWeatherListUseCaseTest { + @MockK + lateinit var repository: WeatherRepository + + @get:Rule + val coroutineRule: MainCoroutineRule = MainCoroutineRule() + + private lateinit var useCase: GetWeatherListUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this) + useCase = spyk(GetWeatherListUseCase(repository, coroutineRule.testDispatcher)) + } + + @Test + @DisplayName("Successful getting weather list") + operator fun invoke() = runBlocking { + val expectedList = arrayListOf( + mockk { every { id } returns 1 }, + mockk { every { id } returns 2 }, + ) + + coEvery { repository.getWeatherList(any(), any(), any()) } returns expectedList + + val result = useCase(1.0, 1.0, 5) + + assertEquals(expectedList, result) + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherUseCaseTest.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherUseCaseTest.kt new file mode 100644 index 0000000..7eb8e2a --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/domain/usecase/GetWeatherUseCaseTest.kt @@ -0,0 +1,48 @@ +package ru.itis.karakurik.androidLab2.domain.usecase + +import io.mockk.* +import ru.itis.karakurik.androidLab2.utils.MainCoroutineRule +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import ru.itis.karakurik.androidLab2.domain.entity.Weather +import ru.itis.karakurik.androidLab2.domain.repository.WeatherRepository + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(JUnit4::class) +internal class GetWeatherUseCaseTest { + @MockK + lateinit var repository: WeatherRepository + + @get:Rule + val coroutineRule: MainCoroutineRule = MainCoroutineRule() + + private lateinit var useCase: GetWeatherUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this) + useCase = spyk(GetWeatherUseCase(repository, coroutineRule.testDispatcher)) + } + + @Test + @DisplayName("Successful getting weather") + operator fun invoke() = runBlocking { + val expected = mockk() { + every { id } returns 1 + } + + coEvery { repository.getWeather(1) } returns expected + + val result = useCase(1) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/cities/CityListViewModelTest.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/cities/CityListViewModelTest.kt new file mode 100644 index 0000000..cb9fb90 --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/cities/CityListViewModelTest.kt @@ -0,0 +1,65 @@ +package ru.itis.karakurik.androidLab2.presentation.fragments.cities + +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.* +import ru.itis.karakurik.androidLab2.domain.entity.Weather +import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherListUseCase +import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherUseCase +import ru.itis.karakurik.androidLab2.utils.getOrAwaitValue + +internal class WeatherViewModelTest { + + @MockK + lateinit var getWeatherUseCase: GetWeatherUseCase + + @MockK + lateinit var getWeatherListUseCase: GetWeatherListUseCase + + private lateinit var viewModel: CityListViewModel + + @Before + fun setUp() { + MockKAnnotations.init(this) + viewModel = CityListViewModel( + getWeatherUseCase, + getWeatherListUseCase + ) + } + + @Test + fun getCityIdByName() = runBlocking { + + val mockCityId = mockk { every { id } returns 1 }.id + val expectedCityId = Result.success(mockCityId) + + coEvery { getWeatherUseCase("Kazan").id } returns mockCityId + + viewModel.onGetCityId("Kazan") + + assertEquals(expectedCityId, viewModel.cityId.getOrAwaitValue()) + } + + @Test + fun getWeatherList() = runBlocking { + + val mockCityList = arrayListOf( + mockk { every { id } returns 1 }, + mockk { every { id } returns 2 }, + ) + + val expectedWeather = Result.success(mockCityList) + + coEvery { getWeatherListUseCase(any(), any(), any()) } returns mockCityList + + viewModel.onGetWeatherList(1.0, 1.0, 5) + + assertEquals(expectedWeather, viewModel.weatherList.getOrAwaitValue()) + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModelTest.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModelTest.kt new file mode 100644 index 0000000..2551387 --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/presentation/fragments/weather/WeatherViewModelTest.kt @@ -0,0 +1,42 @@ +package ru.itis.karakurik.androidLab2.presentation.fragments.weather + +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import ru.itis.karakurik.androidLab2.domain.entity.Weather +import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherUseCase +import ru.itis.karakurik.androidLab2.utils.getOrAwaitValue + +internal class WeatherViewModelTest { + + @MockK + lateinit var getWeatherUseCase: GetWeatherUseCase + + private lateinit var viewModel: WeatherViewModel + + @Before + fun setUp() { + MockKAnnotations.init(this) + viewModel = WeatherViewModel(getWeatherUseCase) + } + + @Test + fun getWeatherById() = runBlocking { + + val mockWeather = mockk { every { id } returns 1 } + val expectedWeather = Result.success(mockWeather) + + coEvery { getWeatherUseCase(1) } returns mockWeather + + viewModel.onGetWeather(1) + + assertEquals(expectedWeather, viewModel.weather.getOrAwaitValue()) + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/utils/MainCoroutineRule.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/utils/MainCoroutineRule.kt new file mode 100644 index 0000000..0b26a40 --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/utils/MainCoroutineRule.kt @@ -0,0 +1,28 @@ +package ru.itis.karakurik.androidLab2.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import kotlin.coroutines.ContinuationInterceptor + +@ExperimentalCoroutinesApi +class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() { + + val testDispatcher = StandardTestDispatcher() + + override fun starting(description: Description) { + super.starting(description) + Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher) + } + + override fun finished(description: Description) { + super.finished(description) + Dispatchers.resetMain() + } +} diff --git a/app/src/test/java/ru/itis/karakurik/androidLab2/utils/getOrAwaitValue.kt b/app/src/test/java/ru/itis/karakurik/androidLab2/utils/getOrAwaitValue.kt new file mode 100644 index 0000000..d466020 --- /dev/null +++ b/app/src/test/java/ru/itis/karakurik/androidLab2/utils/getOrAwaitValue.kt @@ -0,0 +1,35 @@ +package ru.itis.karakurik.androidLab2.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + this.observeForever(observer) + + afterObserve.invoke() + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + this.removeObserver(observer) + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +}