From 89eea684d8d8bae8ac089ccb2ee7aaa20260da74 Mon Sep 17 00:00:00 2001 From: Santiago Mattiauda Date: Tue, 4 Feb 2025 23:35:49 -0300 Subject: [PATCH 1/3] [update] fists tests --- .idea/kotlinc.xml | 2 +- .../com/santimattius/template/di/AppModule.kt | 14 +- .../ui/compose/HomeComposeViewModelDiTest.kt | 131 ++++++++++++++++++ .../ui/compose/HomeComposeViewModelTest.kt | 128 +++++++++++++++++ .../template/ui/rules/KoinTestRule.kt | 48 +++++++ .../template/ui/rules/RoomTestRule.kt | 34 +++++ .../core/data/client/database/MovieDao.kt | 5 +- .../RoomMovieLocalDataSource.kt | 14 +- .../test/rules/MockWebServerRule.kt | 4 +- 9 files changed, 368 insertions(+), 12 deletions(-) create mode 100644 app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt create mode 100644 app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt create mode 100644 app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt create mode 100644 app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb44937..c22b6fa 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/santimattius/template/di/AppModule.kt b/app/src/main/java/com/santimattius/template/di/AppModule.kt index e48fa76..337ba5a 100644 --- a/app/src/main/java/com/santimattius/template/di/AppModule.kt +++ b/app/src/main/java/com/santimattius/template/di/AppModule.kt @@ -5,28 +5,32 @@ 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 + fun provideBaseUrl() = "https://api.themoviedb.org" } \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt new file mode 100644 index 0000000..8d70b5b --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt @@ -0,0 +1,131 @@ +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() + + @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 + val mainCoroutinesTestRule = MainCoroutinesTestRule() + + @Before + fun setUp() { + koinTestRule.loadModule( + module { + //override base url and database + factory(named("base_url")) { mockWebServerRule.baseUrl } + factory { 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 + @Ignore("ignore this test for now") + 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() + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt new file mode 100644 index 0000000..b52406c --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt @@ -0,0 +1,128 @@ +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() + + @get:Rule(order = 0) + val mockWebServerRule = MockWebServerRule { + TheMovieDBServiceMother.createPopularMovieResponse() + } + + @get:Rule(order = 1) + val roomTestRule = RoomTestRule(appContext, TheMovieDataBase::class.java) + + + @get:Rule + 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 + @Ignore("ignore this test for now") + 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() + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt b/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt new file mode 100644 index 0000000..ebb3908 --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt @@ -0,0 +1,48 @@ +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 + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt b/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt new file mode 100644 index 0000000..7b8ca10 --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt @@ -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( + private val appContext: Context, + private val klass: Class +) : TestWatcher() { + + lateinit var db: T + + override fun starting(description: Description?) { + db = Room.inMemoryDatabaseBuilder(appContext, klass) + .allowMainThreadQueries().build() + } + + override fun finished(description: Description?) { + db.close() + } +} \ No newline at end of file diff --git a/core/src/main/java/com/santimattius/core/data/client/database/MovieDao.kt b/core/src/main/java/com/santimattius/core/data/client/database/MovieDao.kt index cf0b41b..7c1b71f 100644 --- a/core/src/main/java/com/santimattius/core/data/client/database/MovieDao.kt +++ b/core/src/main/java/com/santimattius/core/data/client/database/MovieDao.kt @@ -7,7 +7,6 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import androidx.room.Update -import androidx.room.Upsert import com.santimattius.core.data.models.MovieEntity import kotlinx.coroutines.flow.Flow @@ -36,10 +35,10 @@ interface MovieDao { suspend fun update(movies: MovieEntity) @Query("UPDATE movie SET favorite = 1 WHERE id = :movieId") - suspend fun addToFavorite(movieId: Long) + suspend fun addToFavorite(movieId: Long): Int @Query("UPDATE movie SET favorite = 0 WHERE id = :movieId") - suspend fun removeFromFavorite(movieId: Long) + suspend fun removeFromFavorite(movieId: Long): Int @Query( """ diff --git a/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt b/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt index 032c073..56643e0 100644 --- a/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt +++ b/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt @@ -38,11 +38,21 @@ class RoomMovieLocalDataSource( override suspend fun update(movie: MovieEntity) = runSafe { update(movie); true } override suspend fun addToFavorite(movieId: Long): Result { - return runSafe { addToFavorite(movieId) } + return runSafe { + if (findById(movieId) == null) { + throw Throwable("Movie with id $movieId not found") + } + addToFavorite(movieId) + } } override suspend fun removeFromFavorite(movieId: Long): Result { - return runSafe { removeFromFavorite(movieId) } + return runSafe { + if (findById(movieId) == null) { + throw Throwable("Movie not found") + } + removeFromFavorite(movieId) + } } private suspend fun runSafe(block: suspend MovieDao.() -> R) = diff --git a/shared-test/src/main/java/com/santimattius/test/rules/MockWebServerRule.kt b/shared-test/src/main/java/com/santimattius/test/rules/MockWebServerRule.kt index 51e72dc..d9eef1e 100644 --- a/shared-test/src/main/java/com/santimattius/test/rules/MockWebServerRule.kt +++ b/shared-test/src/main/java/com/santimattius/test/rules/MockWebServerRule.kt @@ -7,7 +7,8 @@ import org.junit.runner.Description class MockWebServerRule( private val server: MockWebServer = MockWebServer(), - private val port: Int = 0 + private val port: Int = 0, + private val initialResponse: (() -> MockResponse)? = null ) : TestWatcher() { val baseUrl: String @@ -17,6 +18,7 @@ class MockWebServerRule( override fun starting(description: Description) { super.starting(description) server.start(port) + initialResponse?.let { server.enqueue(it.invoke()) } } override fun finished(description: Description) { From 04b9193af338c19f522807cfef6c12cd58ccc376 Mon Sep 17 00:00:00 2001 From: Santiago Mattiauda Date: Tue, 4 Feb 2025 23:43:00 -0300 Subject: [PATCH 2/3] [update] fixs tests --- app/src/main/java/com/santimattius/template/di/AppModule.kt | 1 + .../template/ui/compose/HomeComposeViewModelDiTest.kt | 2 +- .../template/ui/compose/HomeComposeViewModelTest.kt | 2 +- .../java/com/santimattius/template/ui/rules/KoinTestRule.kt | 6 ++++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/santimattius/template/di/AppModule.kt b/app/src/main/java/com/santimattius/template/di/AppModule.kt index 337ba5a..9280156 100644 --- a/app/src/main/java/com/santimattius/template/di/AppModule.kt +++ b/app/src/main/java/com/santimattius/template/di/AppModule.kt @@ -32,5 +32,6 @@ class AppModule { @Named("base_url") @Singleton + @Suppress("FunctionOnlyReturningConstant") fun provideBaseUrl() = "https://api.themoviedb.org" } \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt index 8d70b5b..5318d79 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt @@ -62,7 +62,7 @@ class HomeComposeViewModelDiTest : KoinTest { } - @get:Rule + @get:Rule(order = 3) val mainCoroutinesTestRule = MainCoroutinesTestRule() @Before diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt index b52406c..a07e442 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt @@ -58,7 +58,7 @@ class HomeComposeViewModelTest { val roomTestRule = RoomTestRule(appContext, TheMovieDataBase::class.java) - @get:Rule + @get:Rule(order = 2) val mainCoroutinesTestRule = MainCoroutinesTestRule() @Before diff --git a/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt b/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt index ebb3908..aa62f76 100644 --- a/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt +++ b/app/src/test/java/com/santimattius/template/ui/rules/KoinTestRule.kt @@ -10,8 +10,10 @@ import org.koin.test.KoinTestRule * 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`. + * @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 From 0d420a4cae463e3363376cf089bb2877048af06e Mon Sep 17 00:00:00 2001 From: Santiago Mattiauda Date: Tue, 4 Feb 2025 23:45:27 -0300 Subject: [PATCH 3/3] [update] update tests --- .../ui/compose/HomeComposeViewModelDiTest.kt | 1 - .../ui/compose/HomeComposeViewModelTest.kt | 1 - .../implementation/RoomMovieLocalDataSource.kt | 14 ++------------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt index 5318d79..cfcaddd 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt @@ -103,7 +103,6 @@ class HomeComposeViewModelDiTest : KoinTest { } @Test - @Ignore("ignore this test for now") fun `onFavorite should update state with success message when removing from favorite succeeds`() { viewModelScenario { HomeComposeViewModel(movieRepository = get()) }.use { val dto = MovieMother.createMovie() diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt index a07e442..617f610 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelTest.kt @@ -100,7 +100,6 @@ class HomeComposeViewModelTest { } @Test - @Ignore("ignore this test for now") fun `onFavorite should update state with success message when removing from favorite succeeds`() { viewModelScenario { HomeComposeViewModel(movieRepository = movieRepository) }.use { val dto = MovieMother.createMovie() diff --git a/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt b/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt index 56643e0..032c073 100644 --- a/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt +++ b/core/src/main/java/com/santimattius/core/data/datasources/implementation/RoomMovieLocalDataSource.kt @@ -38,21 +38,11 @@ class RoomMovieLocalDataSource( override suspend fun update(movie: MovieEntity) = runSafe { update(movie); true } override suspend fun addToFavorite(movieId: Long): Result { - return runSafe { - if (findById(movieId) == null) { - throw Throwable("Movie with id $movieId not found") - } - addToFavorite(movieId) - } + return runSafe { addToFavorite(movieId) } } override suspend fun removeFromFavorite(movieId: Long): Result { - return runSafe { - if (findById(movieId) == null) { - throw Throwable("Movie not found") - } - removeFromFavorite(movieId) - } + return runSafe { removeFromFavorite(movieId) } } private suspend fun runSafe(block: suspend MovieDao.() -> R) =