diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c9b7919..8b8d7ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + implementation(project(Project.cache)) } diff --git a/build.gradle.kts b/build.gradle.kts index f44a8b7..a4f9d3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,8 +28,3 @@ subprojects { tasks.register("clean", Delete::class) { delete(rootProject.buildDir) } -buildscript { - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21") - } -} diff --git a/buildSrc/src/main/kotlin/Extensions.kt b/buildSrc/src/main/kotlin/Extensions.kt index 5cd67cb..bbf4c17 100644 --- a/buildSrc/src/main/kotlin/Extensions.kt +++ b/buildSrc/src/main/kotlin/Extensions.kt @@ -74,7 +74,12 @@ fun DependencyHandler.testImplementation(vararg dependencies: Any) { dependencies.forEach(::testImplementation) } -fun DependencyHandler.kapt(dependencyNotation: String): Dependency? = - add("kapt", dependencyNotation) +fun DependencyHandler.kapt(dependency: Any): Dependency? = add( + "kapt", dependency +) + +fun DependencyHandler.kapt(vararg dependencies: Any) { + dependencies.forEach(::kapt) +} // endregion diff --git a/buildSrc/src/main/kotlin/Library.kt b/buildSrc/src/main/kotlin/Library.kt index 31f38b0..888cf0c 100644 --- a/buildSrc/src/main/kotlin/Library.kt +++ b/buildSrc/src/main/kotlin/Library.kt @@ -11,6 +11,7 @@ object Library { const val navigationUiKtx: String = "androidx.navigation:navigation-ui-ktx:${Version.navigation}" const val room: String = "androidx.room:room-ktx:${Version.room}" + const val roomCompiler: String = "androidx.room:room-compiler:${Version.room}" const val androidxJUnit: String = "androidx.test.ext:junit:${Version.androidxJUnit}" const val espresso: String = "androidx.test.espresso:espresso-core:${Version.espresso}" const val viewModel: String = @@ -53,11 +54,12 @@ object Library { const val truth: String = "com.google.truth:truth:${Version.truth}" // Mockito - const val mockito: String = "org.mockito:mockito-core:${Version.mockito}" + const val mockito = "org.mockito.kotlin:mockito-kotlin:${Version.mockito}" } object Project { const val executor: String = ":executor" const val libExpense: String = ":lib_expense" const val testUtils: String = ":test_utils" + const val cache: String = ":cache" } diff --git a/buildSrc/src/main/kotlin/Version.kt b/buildSrc/src/main/kotlin/Version.kt index c862bb1..810aa4e 100644 --- a/buildSrc/src/main/kotlin/Version.kt +++ b/buildSrc/src/main/kotlin/Version.kt @@ -31,7 +31,7 @@ object Version { const val truth: String = "1.1.3" // Mockito - const val mockito: String = "3.11.2" + const val mockito: String = "3.2.0" // ktlint const val ktlint: String = "0.42.1" diff --git a/buildSrc/src/main/kotlin/extensions/KotlinJvmExtension.kt b/buildSrc/src/main/kotlin/extensions/KotlinJvmExtension.kt index ef02a75..b8f4c87 100644 --- a/buildSrc/src/main/kotlin/extensions/KotlinJvmExtension.kt +++ b/buildSrc/src/main/kotlin/extensions/KotlinJvmExtension.kt @@ -12,8 +12,6 @@ private class KotlinJvmExtension : ProjectExtension { override fun configure(extension: Any) { if (extension !is KotlinJvmOptions) return extension.apply { - freeCompilerArgs += "-Xuse-experimental=" + - "kotlinx.coroutines.ExperimentalCoroutinesApi" freeCompilerArgs += "-Xexplicit-api=strict" } } diff --git a/buildSrc/src/main/kotlin/plugin/AndroidLibraryPlugin.kt b/buildSrc/src/main/kotlin/plugin/AndroidLibraryPlugin.kt index 7540a89..22f5ae9 100644 --- a/buildSrc/src/main/kotlin/plugin/AndroidLibraryPlugin.kt +++ b/buildSrc/src/main/kotlin/plugin/AndroidLibraryPlugin.kt @@ -25,7 +25,8 @@ class AndroidLibraryPlugin : BasePlugin() { get() = { implementation( Library.daggerHiltAndroid, - Library.coroutines + Library.coroutines, + Library.room ) testImplementation( Library.junit, @@ -33,7 +34,10 @@ class AndroidLibraryPlugin : BasePlugin() { Library.mockito, Library.coroutinesTest ) - kapt(Library.daggerHiltCompiler) + kapt( + Library.daggerHiltCompiler, + Library.roomCompiler + ) } override val extensions: Array diff --git a/cache/.gitignore b/cache/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/cache/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts new file mode 100644 index 0000000..d61e778 --- /dev/null +++ b/cache/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + androidLib +} diff --git a/cache/consumer-rules.pro b/cache/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/cache/proguard-rules.pro b/cache/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/cache/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/cache/src/main/AndroidManifest.xml b/cache/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d80a908 --- /dev/null +++ b/cache/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/cache/src/main/java/com/example/expenselogger/cache/ExpenseDatabase.kt b/cache/src/main/java/com/example/expenselogger/cache/ExpenseDatabase.kt new file mode 100644 index 0000000..373c049 --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/ExpenseDatabase.kt @@ -0,0 +1,23 @@ +package com.example.expenselogger.cache + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.example.expenselogger.cache.dao.ExpenseDao +import com.example.expenselogger.cache.entity.ExpenseEntity + +@Database(entities = [ExpenseEntity::class], version = 1, exportSchema = false) +internal abstract class ExpenseDatabase : RoomDatabase() { + + abstract val expenseDao: ExpenseDao + + companion object { + private const val DATABASE_NAME: String = "expense_db" + fun build(context: Context): ExpenseDatabase = Room.databaseBuilder( + context.applicationContext, + ExpenseDatabase::class.java, + DATABASE_NAME + ).build() + } +} diff --git a/cache/src/main/java/com/example/expenselogger/cache/dao/ExpenseDao.kt b/cache/src/main/java/com/example/expenselogger/cache/dao/ExpenseDao.kt new file mode 100644 index 0000000..1001633 --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/dao/ExpenseDao.kt @@ -0,0 +1,25 @@ +package com.example.expenselogger.cache.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.example.expenselogger.cache.entity.ExpenseEntity + +@Dao +internal interface ExpenseDao { + @Insert + fun insertExpense(expense: ExpenseEntity) + + @Update + fun updateExpense(expense: ExpenseEntity) + + @Query("SELECT * FROM expense WHERE id = :id") + fun getExpense(id: Long): ExpenseEntity + + @Query("SELECT * FROM expense ORDER BY date ASC") + public fun getExpenses(): List + + @Query("DELETE FROM expense WHERE id = :id") + fun deleteExpense(id: Long) +} diff --git a/cache/src/main/java/com/example/expenselogger/cache/di/CacheModule.kt b/cache/src/main/java/com/example/expenselogger/cache/di/CacheModule.kt new file mode 100644 index 0000000..40b951f --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/di/CacheModule.kt @@ -0,0 +1,33 @@ +package com.example.expenselogger.cache.di + +import android.content.Context +import com.example.expenselogger.cache.ExpenseDatabase +import com.example.expenselogger.cache.dao.ExpenseDao +import com.example.expenselogger.cache.repository.ExpenseRepository +import com.example.expenselogger.cache.repository.ExpenseRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@[Module InstallIn(SingletonComponent::class)] +internal interface CacheModule { + + @get:Binds + val ExpenseRepositoryImpl.expenseRepository: ExpenseRepository + + companion object { + @[Provides Singleton] + fun providesExpenseDatabase( + @ApplicationContext context: Context + ): ExpenseDatabase = ExpenseDatabase.build(context) + + @[Provides Singleton] + fun providesExpenseDao( + expenseDatabase: ExpenseDatabase + ): ExpenseDao = expenseDatabase.expenseDao + } +} diff --git a/cache/src/main/java/com/example/expenselogger/cache/entity/ExpenseEntity.kt b/cache/src/main/java/com/example/expenselogger/cache/entity/ExpenseEntity.kt new file mode 100644 index 0000000..c7f905d --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/entity/ExpenseEntity.kt @@ -0,0 +1,20 @@ +package com.example.expenselogger.cache.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "expense") +public data class ExpenseEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + val id: Long, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "amount") + val amount: Double, + @ColumnInfo(name = "date") + val date: Long, + @ColumnInfo(name = "info") + val info: String +) diff --git a/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepository.kt b/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepository.kt new file mode 100644 index 0000000..38bdb20 --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepository.kt @@ -0,0 +1,12 @@ +package com.example.expenselogger.cache.repository + +import com.example.expenselogger.cache.entity.ExpenseEntity + +public interface ExpenseRepository { + + public fun insertExpense(expenseEntity: ExpenseEntity) + public fun updateExpense(expenseEntity: ExpenseEntity) + public fun getExpense(id: Long): ExpenseEntity? + public fun getExpenses(): List + public fun deleteExpense(id: Long) +} diff --git a/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepositoryImpl.kt b/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepositoryImpl.kt new file mode 100644 index 0000000..3f376ea --- /dev/null +++ b/cache/src/main/java/com/example/expenselogger/cache/repository/ExpenseRepositoryImpl.kt @@ -0,0 +1,30 @@ +package com.example.expenselogger.cache.repository + +import com.example.expenselogger.cache.dao.ExpenseDao +import com.example.expenselogger.cache.entity.ExpenseEntity +import javax.inject.Inject + +internal class ExpenseRepositoryImpl @Inject constructor( + private val expenseDao: ExpenseDao +) : ExpenseRepository { + + override fun insertExpense(expenseEntity: ExpenseEntity) { + expenseDao.insertExpense(expenseEntity) + } + + override fun updateExpense(expenseEntity: ExpenseEntity) { + expenseDao.updateExpense(expenseEntity) + } + + override fun getExpense(id: Long): ExpenseEntity? { + return expenseDao.getExpense(id) + } + + override fun getExpenses(): List { + return expenseDao.getExpenses() + } + + override fun deleteExpense(id: Long) { + expenseDao.deleteExpense(id) + } +} diff --git a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/DeleteExpenseTest.kt b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/DeleteExpenseTest.kt index d2119ca..1be504f 100644 --- a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/DeleteExpenseTest.kt +++ b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/DeleteExpenseTest.kt @@ -1,26 +1,29 @@ package com.example.expenselogger.lib_expense.domain.usecase -import com.example.expenselogger.lib_expense.domain.fakes.FakeExpenseContract +import com.example.expenselogger.lib_expense.domain.contract.ExpenseContract import com.example.expenselogger.lib_expense.domain.model.DummyData import com.example.expenselogger_test_utils.TestAsyncExecutor -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before import org.junit.Test +import org.mockito.Mockito.verify +import org.mockito.kotlin.mock public class DeleteExpenseTest { - private val expenseContract = FakeExpenseContract() - private val deleteExpense = DeleteExpense( - expenseContract, - TestAsyncExecutor() - ) + private lateinit var expenseContract: ExpenseContract + private lateinit var deleteExpense: DeleteExpense + + @Before + public fun setup() { + expenseContract = mock() + deleteExpense = DeleteExpense(expenseContract, TestAsyncExecutor()) + } @Test - public fun `verify that deleteExpense usecase deletes an expense if it exists`(): Unit = - runBlockingTest { - val expense = DummyData.expense - expenseContract.saveExpense(expense) - deleteExpense.invoke(expense) - assertThat(expenseContract.getExpenses().size).isEqualTo(0) - } + public fun `verify that deleteExpense usecase deletes an expense`(): Unit = runBlockingTest { + val expense = DummyData.expense + deleteExpense.invoke(expense) + verify(expenseContract).deleteExpense(expense) + } } diff --git a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpenseTest.kt b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpenseTest.kt index b4d979c..bcb5c62 100644 --- a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpenseTest.kt +++ b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpenseTest.kt @@ -1,26 +1,24 @@ package com.example.expenselogger.lib_expense.domain.usecase -import com.example.expenselogger.lib_expense.domain.fakes.FakeExpenseContract +import com.example.expenselogger.lib_expense.domain.contract.ExpenseContract import com.example.expenselogger.lib_expense.domain.model.DummyData import com.example.expenselogger_test_utils.TestAsyncExecutor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runBlockingTest import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever public class GetExpenseTest { - private val expenseContract = FakeExpenseContract() - private val getExpense = GetExpense( - expenseContract, - TestAsyncExecutor() - ) - @Test - public fun `verify that getExpense usecase returns an expense`(): Unit = - runBlockingTest { - val expense = DummyData.expense - expenseContract.saveExpense(expense) - val expectedExpense = getExpense.invoke(expense.id) - assertThat(expectedExpense).isEqualTo(expense) + public fun `verify that getExpense usecase gets an expense`(): Unit = runBlockingTest { + val expense = DummyData.expense + val expenseContract = mock().apply { + whenever(getExpense(0)).thenReturn(expense) } + val getExpense = GetExpense(expenseContract, TestAsyncExecutor()) + val expectedExpense = getExpense.invoke(0) + assertThat(expectedExpense).isEqualTo(expense) + } } diff --git a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpensesTest.kt b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpensesTest.kt index 687f853..641ac0d 100644 --- a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpensesTest.kt +++ b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/GetExpensesTest.kt @@ -1,25 +1,24 @@ package com.example.expenselogger.lib_expense.domain.usecase -import com.example.expenselogger.lib_expense.domain.fakes.FakeExpenseContract +import com.example.expenselogger.lib_expense.domain.contract.ExpenseContract import com.example.expenselogger.lib_expense.domain.model.DummyData import com.example.expenselogger_test_utils.TestAsyncExecutor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runBlockingTest import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever public class GetExpensesTest { - private val expenseContract = FakeExpenseContract() - private val getExpenses = GetExpenses( - expenseContract, - TestAsyncExecutor() - ) - @Test - public fun `verify that expenses returns a list of expenses`(): Unit = runBlockingTest { - val expense = DummyData.expense - expenseContract.saveExpense(expense) + public fun `verify that getExpenses usecase gets a list of expenses`(): Unit = runBlockingTest { + val expenses = listOf(DummyData.expense) + val expenseContract = mock().apply { + whenever(getExpenses()).thenReturn(expenses) + } + val getExpenses = GetExpenses(expenseContract, TestAsyncExecutor()) val expectedExpenses = getExpenses.invoke() - assertThat(expectedExpenses.size).isEqualTo(1) + assertThat(expectedExpenses).isEqualTo(expenses) } } diff --git a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/SaveExpenseTest.kt b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/SaveExpenseTest.kt index 6667c85..21ef070 100644 --- a/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/SaveExpenseTest.kt +++ b/lib_expense/src/test/java/com/example/expenselogger/lib_expense/domain/usecase/SaveExpenseTest.kt @@ -1,25 +1,29 @@ package com.example.expenselogger.lib_expense.domain.usecase -import com.example.expenselogger.lib_expense.domain.fakes.FakeExpenseContract +import com.example.expenselogger.lib_expense.domain.contract.ExpenseContract import com.example.expenselogger.lib_expense.domain.model.DummyData import com.example.expenselogger_test_utils.TestAsyncExecutor -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before import org.junit.Test +import org.mockito.Mockito.verify +import org.mockito.kotlin.mock public class SaveExpenseTest { - private val expenseContract = FakeExpenseContract() - private val saveExpense = SaveExpense( - expenseContract, - TestAsyncExecutor() - ) + private lateinit var expenseContract: ExpenseContract + private lateinit var saveExpense: SaveExpense + + @Before + public fun setup() { + expenseContract = mock() + saveExpense = SaveExpense(expenseContract, TestAsyncExecutor()) + } @Test public fun `verify that saveExpense usecase saves an expense`(): Unit = runBlockingTest { val expense = DummyData.expense saveExpense.invoke(expense) - val savedExpense = expenseContract.getExpense(expense.id) - assertThat(savedExpense).isEqualTo(expense) + verify(expenseContract).saveExpense(expense) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index c181e8a..b83790a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,5 +3,6 @@ include( ":app", ":lib_expense", ":executor", - ":test_utils" + ":test_utils", + ":cache" )