Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

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

15 changes: 10 additions & 5 deletions app/src/main/java/com/santimattius/template/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@ import com.santimattius.core.data.client.database.TheMovieDataBase
import com.santimattius.core.data.client.network.RetrofitServiceCreator
import com.santimattius.core.data.client.network.TheMovieDBService
import com.santimattius.template.BuildConfig
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
import org.koin.core.annotation.Singleton


@Module
class AppModule {

@Factory
@Singleton
fun provideAppDatabase(context: Context): TheMovieDataBase =
TheMovieDataBase.get(context)

@Factory
@Singleton
fun provideMovieDBService(serviceCreator: RetrofitServiceCreator): TheMovieDBService =
serviceCreator.createService(TheMovieDBService::class.java)


@Singleton
fun provideRetrofit(): RetrofitServiceCreator {
fun provideRetrofit(@Named("base_url") baseUrl: String): RetrofitServiceCreator {
return RetrofitServiceCreator(
baseUrl = "https://api.themoviedb.org",
baseUrl = baseUrl,
apiKey = BuildConfig.apiKey
)
}

@Named("base_url")
@Singleton
@Suppress("FunctionOnlyReturningConstant")
fun provideBaseUrl() = "https://api.themoviedb.org"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.santimattius.template.ui.compose

import android.app.Application
import android.os.Build
import androidx.lifecycle.viewmodel.testing.viewModelScenario
import androidx.test.core.app.ApplicationProvider
import app.cash.turbine.test
import com.santimattius.core.data.client.database.TheMovieDataBase
import com.santimattius.template.ui.compose.models.Messages
import com.santimattius.template.ui.models.MovieUiModel
import com.santimattius.template.ui.rules.RoomTestRule
import com.santimattius.template.ui.rules.loadModule
import com.santimattius.test.data.MovieMother
import com.santimattius.test.data.TheMovieDBServiceMother
import com.santimattius.test.rules.MainCoroutinesTestRule
import com.santimattius.test.rules.MockWebServerRule
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koin.ksp.generated.com_santimattius_template_di_AppModule
import org.koin.ksp.generated.com_santimattius_template_di_DataModule
import org.koin.ksp.generated.defaultModule
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import org.koin.test.get
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
sdk = [Build.VERSION_CODES.R],
instrumentedPackages = ["androidx.loader.content"],
application = Application::class
)
class HomeComposeViewModelDiTest : KoinTest {

private val appContext = ApplicationProvider.getApplicationContext<Application>()

@get:Rule(order = 0)
val mockWebServerRule = MockWebServerRule {
TheMovieDBServiceMother.createPopularMovieResponse()
}

@get:Rule(order = 1)
val roomTestRule = RoomTestRule(appContext, TheMovieDataBase::class.java)


@get:Rule(order = 2)
var koinTestRule = KoinTestRule.create {
modules(
com_santimattius_template_di_DataModule,
com_santimattius_template_di_AppModule,
defaultModule,
)
}


@get:Rule(order = 3)
val mainCoroutinesTestRule = MainCoroutinesTestRule()

@Before
fun setUp() {
koinTestRule.loadModule(
module {
//override base url and database
factory(named("base_url")) { mockWebServerRule.baseUrl }
factory<TheMovieDataBase> { roomTestRule.db }
}
)
}

@Test
fun `onFavorite should update state with success message when adding to favorite succeeds`() {
viewModelScenario { HomeComposeViewModel(movieRepository = get()) }.use {
val dto = MovieMother.createMovie()
val movie = MovieUiModel(
id = dto.id,
title = dto.title,
imageUrl = dto.poster,
favorite = false
)
val viewModel = it.viewModel

runTest(mainCoroutinesTestRule.testDispatcher) {
viewModel.onFavorite(movie)
viewModel.state.test {
awaitItem()
assertEquals(
Messages.Success("Add ${movie.title} to favorite"),
awaitItem().successMessages
)
cancelAndIgnoreRemainingEvents()
}
}
}
}

@Test
fun `onFavorite should update state with success message when removing from favorite succeeds`() {
viewModelScenario { HomeComposeViewModel(movieRepository = get()) }.use {
val dto = MovieMother.createMovie()
val movie = MovieUiModel(
id = dto.id,
title = dto.title,
imageUrl = dto.poster,
favorite = true
)
val viewModel = it.viewModel
runTest(mainCoroutinesTestRule.testDispatcher) {
viewModel.onFavorite(movie)
viewModel.state.test {
awaitItem()
assertEquals(
Messages.Success("Remove ${movie.title} from favorite"),
awaitItem().successMessages
)
cancelAndIgnoreRemainingEvents()
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.santimattius.template.ui.compose

import android.app.Application
import android.os.Build
import androidx.lifecycle.viewmodel.testing.viewModelScenario
import androidx.test.core.app.ApplicationProvider
import app.cash.turbine.test
import com.santimattius.core.data.client.database.TheMovieDataBase
import com.santimattius.core.data.client.network.RetrofitServiceCreator
import com.santimattius.core.data.client.network.TheMovieDBService
import com.santimattius.core.data.datasources.implementation.RetrofitMovieNetworkDataSource
import com.santimattius.core.data.datasources.implementation.RoomMovieLocalDataSource
import com.santimattius.core.data.repositories.TMDbRepository
import com.santimattius.template.ui.compose.models.Messages
import com.santimattius.template.ui.models.MovieUiModel
import com.santimattius.template.ui.rules.RoomTestRule
import com.santimattius.template.ui.rules.loadModule
import com.santimattius.test.data.MovieMother
import com.santimattius.test.data.TheMovieDBServiceMother
import com.santimattius.test.rules.MainCoroutinesTestRule
import com.santimattius.test.rules.MockWebServerRule
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koin.ksp.generated.com_santimattius_template_di_AppModule
import org.koin.ksp.generated.com_santimattius_template_di_DataModule
import org.koin.ksp.generated.defaultModule
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import org.koin.test.get
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
sdk = [Build.VERSION_CODES.R],
instrumentedPackages = ["androidx.loader.content"],
application = Application::class
)
class HomeComposeViewModelTest {

private lateinit var movieRepository: TMDbRepository
private val appContext = ApplicationProvider.getApplicationContext<Application>()

@get:Rule(order = 0)
val mockWebServerRule = MockWebServerRule {
TheMovieDBServiceMother.createPopularMovieResponse()
}

@get:Rule(order = 1)
val roomTestRule = RoomTestRule(appContext, TheMovieDataBase::class.java)


@get:Rule(order = 2)
val mainCoroutinesTestRule = MainCoroutinesTestRule()

@Before
fun setUp() {
val movieLocalDataSource = RoomMovieLocalDataSource(roomTestRule.db)
val serviceCreator = RetrofitServiceCreator(mockWebServerRule.baseUrl, "test")
val movieNetworkDataSource = RetrofitMovieNetworkDataSource(
serviceCreator.createService(
TheMovieDBService::class.java
)
)
movieRepository = TMDbRepository(movieNetworkDataSource, movieLocalDataSource)
}

@Test
fun `onFavorite should update state with success message when adding to favorite succeeds`() {
viewModelScenario { HomeComposeViewModel(movieRepository = movieRepository) }.use {
val dto = MovieMother.createMovie()
val movie = MovieUiModel(
id = dto.id,
title = dto.title,
imageUrl = dto.poster,
favorite = false
)
val viewModel = it.viewModel

runTest(mainCoroutinesTestRule.testDispatcher) {
viewModel.onFavorite(movie)
viewModel.state.test {
awaitItem()
assertEquals(
Messages.Success("Add ${movie.title} to favorite"),
awaitItem().successMessages
)
cancelAndIgnoreRemainingEvents()
}
}
}
}

@Test
fun `onFavorite should update state with success message when removing from favorite succeeds`() {
viewModelScenario { HomeComposeViewModel(movieRepository = movieRepository) }.use {
val dto = MovieMother.createMovie()
val movie = MovieUiModel(
id = dto.id,
title = dto.title,
imageUrl = dto.poster,
favorite = true
)
val viewModel = it.viewModel
runTest(mainCoroutinesTestRule.testDispatcher) {
viewModel.onFavorite(movie)
viewModel.state.test {
awaitItem()
assertEquals(
Messages.Success("Remove ${movie.title} from favorite"),
awaitItem().successMessages
)
cancelAndIgnoreRemainingEvents()
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.santimattius.template.ui.rules

import org.koin.core.module.Module
import org.koin.test.KoinTestRule

/**
* Loads a single Koin [Module] into the Koin container within the scope of this [KoinTestRule].
*
* This function simplifies the process of loading a module specifically for testing purposes.
* It delegates the actual loading process to Koin's `loadModules` function.
*
* @param module The Koin [Module] to be loaded.
* @param allowOverride If `true`, allows definitions in the loaded module to override
* existing definitions. Defaults to `true`.
* @param createEagerInstances If `true`, forces the creation of all eager instances
* defined in the loaded module. Defaults to `false`.
*
* @see org.koin.core.Koin.loadModules
* @see org.koin.core.module.Module
* @see org.koin.test.KoinTestRule
*
* Example Usage:
* ```
* val myModule = module {
* single { MyDependency() }
* }
*
* @get:Rule
* val koinTestRule = KoinTestRule.create {
* // ... other configurations
* }
*
* @Test
* fun testMyComponent() {
* koinTestRule.loadModule(myModule)
* // ... now the module is loaded and ready for usage
* }
* ```
*/
fun KoinTestRule.loadModule(
module: Module,
allowOverride: Boolean = true,
createEagerInstances: Boolean = false
) {
koin.loadModules(
modules = listOf(module),
allowOverride = allowOverride,
createEagerInstances = createEagerInstances
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.santimattius.template.ui.rules

import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import org.junit.rules.TestWatcher
import org.junit.runner.Description

/**
* A JUnit TestRule for testing Room database interactions.
*
* This rule provides an in-memory instance of a Room database for testing purposes.
* It handles the setup and teardown of the database, ensuring a clean state for each test.
*
* @param appContext The application context used to build the in-memory database.
* @param klass The class of the Room database to be tested (e.g., `MyDatabase::class.java`).
* @param T The type of the Room database, which must extend [RoomDatabase].
*/
class RoomTestRule<T : RoomDatabase>(
private val appContext: Context,
private val klass: Class<T>
) : TestWatcher() {

lateinit var db: T

override fun starting(description: Description?) {
db = Room.inMemoryDatabaseBuilder(appContext, klass)
.allowMainThreadQueries().build()
}

override fun finished(description: Description?) {
db.close()
}
}
Loading