feat(db): Room-Datenbank & DAOs implementieren (#18)
- KrisenvorratDatabase mit allen 4 Entities und LocalDateConverter - CategoryDao, LocationDao, ItemDao, SettingsDao mit CRUD und Flow-Queries - ItemDao.getExpiringSoon(daysUntil) als Default-Interface-Methode - SettingsDao mit @Upsert (Room 2.6.1) - Instrumentierungstests für alle 4 DAOs (in-memory DB) - androidx.room:room-testing zu Dependencies ergänzt
This commit is contained in:
parent
f52724ce0c
commit
b719739451
11 changed files with 539 additions and 0 deletions
|
|
@ -71,6 +71,7 @@ dependencies {
|
|||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class CategoryDaoTest {
|
||||
|
||||
private lateinit var db: KrisenvorratDatabase
|
||||
private lateinit var dao: CategoryDao
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
KrisenvorratDatabase::class.java
|
||||
).allowMainThreadQueries().build()
|
||||
dao = db.categoryDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_insert_emptyDb_getAllReturnsOneItem() = runBlocking {
|
||||
// Given
|
||||
val category = CategoryEntity(name = "Lebensmittel")
|
||||
|
||||
// When
|
||||
dao.insert(category)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("Lebensmittel", result.first().name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_update_existingCategory_nameIsChanged() = runBlocking {
|
||||
// Given
|
||||
dao.insert(CategoryEntity(name = "Alt"))
|
||||
val inserted = dao.getAll().first().first()
|
||||
|
||||
// When
|
||||
dao.update(inserted.copy(name = "Neu"))
|
||||
|
||||
// Then
|
||||
val updated = dao.getById(inserted.id)
|
||||
assertEquals("Neu", updated?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_delete_existingCategory_getAllReturnsEmpty() = runBlocking {
|
||||
// Given
|
||||
dao.insert(CategoryEntity(name = "Temp"))
|
||||
val inserted = dao.getAll().first().first()
|
||||
|
||||
// When
|
||||
dao.delete(inserted)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getById_nonExistentId_returnsNull() = runBlocking {
|
||||
// Given / When
|
||||
val result = dao.getById(999)
|
||||
|
||||
// Then
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.time.LocalDate
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class ItemDaoTest {
|
||||
|
||||
private lateinit var db: KrisenvorratDatabase
|
||||
private lateinit var dao: ItemDao
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
KrisenvorratDatabase::class.java
|
||||
).allowMainThreadQueries().build()
|
||||
dao = db.itemDao()
|
||||
runBlocking {
|
||||
db.categoryDao().insert(CategoryEntity(id = 1, name = "Kategorie"))
|
||||
db.locationDao().insert(LocationEntity(id = 1, name = "Ort"))
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
private fun buildItem(
|
||||
id: String = "item-1",
|
||||
categoryId: Int = 1,
|
||||
locationId: Int = 1,
|
||||
quantity: Double = 1.0,
|
||||
expiryDate: LocalDate? = null
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = "Wasser",
|
||||
categoryId = categoryId,
|
||||
quantity = quantity,
|
||||
unit = "Liter",
|
||||
unitPrice = 0.5,
|
||||
kcalPer100g = null,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = 0.0,
|
||||
notes = "",
|
||||
lastUpdated = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun test_insert_emptyDb_getAllReturnsOneItem() = runBlocking {
|
||||
// Given
|
||||
val item = buildItem()
|
||||
|
||||
// When
|
||||
dao.insert(item)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("item-1", result.first().id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_update_existingItem_quantityIsChanged() = runBlocking {
|
||||
// Given
|
||||
dao.insert(buildItem(quantity = 1.0))
|
||||
|
||||
// When
|
||||
dao.update(buildItem(quantity = 5.0))
|
||||
|
||||
// Then
|
||||
val updated = dao.getById("item-1")
|
||||
assertEquals(5.0, updated?.quantity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_delete_existingItem_getAllReturnsEmpty() = runBlocking {
|
||||
// Given
|
||||
val item = buildItem()
|
||||
dao.insert(item)
|
||||
|
||||
// When
|
||||
dao.delete(item)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getByLocation_twoItemsDifferentLocations_returnsOnlyMatching() = runBlocking {
|
||||
// Given
|
||||
db.locationDao().insert(LocationEntity(id = 2, name = "Anderer Ort"))
|
||||
dao.insert(buildItem(id = "item-1", locationId = 1))
|
||||
dao.insert(buildItem(id = "item-2", locationId = 2))
|
||||
|
||||
// When
|
||||
val result = dao.getByLocation(1).first()
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("item-1", result.first().id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getByCategory_twoItemsDifferentCategories_returnsOnlyMatching() = runBlocking {
|
||||
// Given
|
||||
db.categoryDao().insert(CategoryEntity(id = 2, name = "Andere"))
|
||||
dao.insert(buildItem(id = "item-1", categoryId = 1))
|
||||
dao.insert(buildItem(id = "item-2", categoryId = 2))
|
||||
|
||||
// When
|
||||
val result = dao.getByCategory(1).first()
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("item-1", result.first().id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getExpiringSoon_itemExpiresInOneDayWithDaysUntil365_returnsItem() = runBlocking {
|
||||
// Given
|
||||
val tomorrow = LocalDate.now().plusDays(1)
|
||||
dao.insert(buildItem(expiryDate = tomorrow))
|
||||
|
||||
// When
|
||||
val result = dao.getExpiringSoon(365).first()
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("item-1", result.first().id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class LocationDaoTest {
|
||||
|
||||
private lateinit var db: KrisenvorratDatabase
|
||||
private lateinit var dao: LocationDao
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
KrisenvorratDatabase::class.java
|
||||
).allowMainThreadQueries().build()
|
||||
dao = db.locationDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_insert_emptyDb_getAllReturnsOneItem() = runBlocking {
|
||||
// Given
|
||||
val location = LocationEntity(name = "Keller")
|
||||
|
||||
// When
|
||||
dao.insert(location)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("Keller", result.first().name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_update_existingLocation_nameIsChanged() = runBlocking {
|
||||
// Given
|
||||
dao.insert(LocationEntity(name = "Alt"))
|
||||
val inserted = dao.getAll().first().first()
|
||||
|
||||
// When
|
||||
dao.update(inserted.copy(name = "Neu"))
|
||||
|
||||
// Then
|
||||
val updated = dao.getById(inserted.id)
|
||||
assertEquals("Neu", updated?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_delete_existingLocation_getAllReturnsEmpty() = runBlocking {
|
||||
// Given
|
||||
dao.insert(LocationEntity(name = "Temp"))
|
||||
val inserted = dao.getAll().first().first()
|
||||
|
||||
// When
|
||||
dao.delete(inserted)
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getById_nonExistentId_returnsNull() = runBlocking {
|
||||
// Given / When
|
||||
val result = dao.getById(999)
|
||||
|
||||
// Then
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class SettingsDaoTest {
|
||||
|
||||
private lateinit var db: KrisenvorratDatabase
|
||||
private lateinit var dao: SettingsDao
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
KrisenvorratDatabase::class.java
|
||||
).allowMainThreadQueries().build()
|
||||
dao = db.settingsDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_upsert_newKey_getValueReturnsValue() = runBlocking {
|
||||
// Given
|
||||
val setting = SettingsEntity(key = "theme", value = "dark")
|
||||
|
||||
// When
|
||||
dao.upsert(setting)
|
||||
|
||||
// Then
|
||||
val result = dao.getValue("theme")
|
||||
assertEquals("dark", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getValue_unknownKey_returnsNull() = runBlocking {
|
||||
// Given / When
|
||||
val result = dao.getValue("nonexistent")
|
||||
|
||||
// Then
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_upsert_existingKey_getAllReturnsOnlyOneEntry() = runBlocking {
|
||||
// Given
|
||||
dao.upsert(SettingsEntity(key = "theme", value = "dark"))
|
||||
|
||||
// When
|
||||
dao.upsert(SettingsEntity(key = "theme", value = "light"))
|
||||
|
||||
// Then
|
||||
val result = dao.getAll().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("light", result.first().value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package de.krisenvorrat.app.data.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
||||
import de.krisenvorrat.app.data.db.dao.ItemDao
|
||||
import de.krisenvorrat.app.data.db.dao.LocationDao
|
||||
import de.krisenvorrat.app.data.db.dao.SettingsDao
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
|
||||
@Database(
|
||||
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(LocalDateConverter::class)
|
||||
internal abstract class KrisenvorratDatabase : RoomDatabase() {
|
||||
abstract fun categoryDao(): CategoryDao
|
||||
abstract fun locationDao(): LocationDao
|
||||
abstract fun itemDao(): ItemDao
|
||||
abstract fun settingsDao(): SettingsDao
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
internal interface CategoryDao {
|
||||
|
||||
@Insert
|
||||
suspend fun insert(category: CategoryEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(category: CategoryEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(category: CategoryEntity)
|
||||
|
||||
@Query("SELECT * FROM categories")
|
||||
fun getAll(): Flow<List<CategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM categories WHERE id = :id")
|
||||
suspend fun getById(id: Int): CategoryEntity?
|
||||
}
|
||||
41
app/src/main/java/de/krisenvorrat/app/data/db/dao/ItemDao.kt
Normal file
41
app/src/main/java/de/krisenvorrat/app/data/db/dao/ItemDao.kt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.LocalDate
|
||||
|
||||
@Dao
|
||||
internal interface ItemDao {
|
||||
|
||||
@Insert
|
||||
suspend fun insert(item: ItemEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(item: ItemEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(item: ItemEntity)
|
||||
|
||||
@Query("SELECT * FROM items")
|
||||
fun getAll(): Flow<List<ItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM items WHERE id = :id")
|
||||
suspend fun getById(id: String): ItemEntity?
|
||||
|
||||
@Query("SELECT * FROM items WHERE category_id = :categoryId")
|
||||
fun getByCategory(categoryId: Int): Flow<List<ItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM items WHERE location_id = :locationId")
|
||||
fun getByLocation(locationId: Int): Flow<List<ItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM items WHERE expiry_date IS NOT NULL AND expiry_date <= :cutoff")
|
||||
fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>>
|
||||
|
||||
fun getExpiringSoon(daysUntil: Int = 30): Flow<List<ItemEntity>> =
|
||||
getExpiringSoonByCutoff(LocalDate.now().plusDays(daysUntil.toLong()))
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
internal interface LocationDao {
|
||||
|
||||
@Insert
|
||||
suspend fun insert(location: LocationEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(location: LocationEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(location: LocationEntity)
|
||||
|
||||
@Query("SELECT * FROM locations")
|
||||
fun getAll(): Flow<List<LocationEntity>>
|
||||
|
||||
@Query("SELECT * FROM locations WHERE id = :id")
|
||||
suspend fun getById(id: Int): LocationEntity?
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.krisenvorrat.app.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
internal interface SettingsDao {
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(setting: SettingsEntity)
|
||||
|
||||
@Query("SELECT value FROM settings WHERE `key` = :key")
|
||||
suspend fun getValue(key: String): String?
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAll(): Flow<List<SettingsEntity>>
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
|
|||
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue