- room database migration in progress

This commit is contained in:
2023-09-08 14:59:54 +01:00
parent 1258afc4d0
commit 220afa04cf
24 changed files with 695 additions and 188 deletions

View File

@@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-kapt'
def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD")
@@ -17,6 +18,12 @@ android {
versionName "2.1"
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
signingConfigs {
release {
@@ -33,6 +40,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
androidTest.assets.srcDirs +=
files("$projectDir/schemas".toString())
}
useLibrary 'android.test.mock'
}
@@ -77,6 +88,8 @@ dependencies {
/ * Room database * /
runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION"
implementation "androidx.room:room-ktx:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION"
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
/ *Kodein Dependency Injection * /
implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION"
implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION"

View File

@@ -1,17 +1,19 @@
package com.appttude.h_mal.farmr.application
import androidx.room.Room
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.base.BaseApplication
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.model.Shift
class TestAppClass : BaseApplication() {
private val idlingResources = CountingIdlingResource("Data_loader")
lateinit var database: LegacyDatabase
lateinit var database: AppDatabase
lateinit var preferenceProvider: PreferenceProvider
override fun onCreate() {
@@ -19,9 +21,9 @@ class TestAppClass : BaseApplication() {
IdlingRegistry.getInstance().register(idlingResources)
}
override fun createDatabase(): LegacyDatabase {
database =
LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver)
override fun createDatabase(): AppDatabase {
database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
.build()
return database
}
@@ -30,9 +32,9 @@ class TestAppClass : BaseApplication() {
return preferenceProvider
}
fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift)
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.forEach { addToDatabase(it) }
fun clearDatabase() = database.deleteAllShiftsInDatabase()
fun addToDatabase(shift: Shift) = database.getShiftDao().upsertFullShift(shift.convertToShiftEntity())
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.map { it.convertToShiftEntity() }.let { database.getShiftDao().upsertListOfFullShift(it) }
fun clearDatabase() = database.getShiftDao().deleteAllShifts()
fun cleanPrefs() = preferenceProvider.clearPrefs()
}

View File

@@ -0,0 +1,68 @@
package com.appttude.h_mal.farmr.data.room
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.testing.MigrationTestHelper
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.ui.utils.getShifts
import org.junit.Rule
import org.junit.Test
import java.io.IOException
class RoomMigrationTest {
private val TEST_DB = "migration-test"
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName
)
@Test
@Throws(IOException::class)
fun migrationFrom2To3_containsCorrectData() {
// Create the database in version 4
val db = testHelper.createDatabase(TEST_DB, 4)
// Insert some data
getShifts().forEach {
db.insert(
ShiftsContract.ShiftsEntry.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, ContentValues().apply {
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE, it.type.type)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, it.description)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE, it.date)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN, it.timeIn ?: "00:00")
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT, it.timeOut ?: "00:00")
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION, it.duration ?: 0.00f)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK, it.breakMins ?: 0)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT, it.units ?: 0.00f)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE, it.rateOfPay)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY, it.totalPay)
})
}
//Prepare for the next version
db.close()
// Re-open the database with version 5 and provide MIGRATION_4_5
// and MIGRATION_4_5 as the migration process.
testHelper.runMigrationsAndValidate(
TEST_DB, 5,
true, MIGRATION_4_5
)
// MigrationTestHelper automatically verifies the schema
//changes, but not the data validity
// Validate that the data was migrated properly.
val dbUser: User = getMigratedRoomDatabase().userDao().getUser()
assertEquals(dbUser.getId(), USER.getId())
assertEquals(dbUser.getUserName(), USER.getUserName())
// The date was missing in version 2, so it should be null in
//version 3
assertEquals(dbUser.getDate(), null)
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Application
import com.appttude.h_mal.farmr.data.RepositoryImpl
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
@@ -26,6 +27,6 @@ abstract class BaseApplication() : Application(), KodeinAware {
bind() from provider { ApplicationViewModelFactory(instance()) }
}
abstract fun createDatabase(): LegacyDatabase
abstract fun createDatabase(): AppDatabase
abstract fun createPrefs(): PreferenceProvider
}

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.farmr.data
import androidx.lifecycle.LiveData
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.Sortable
@@ -8,8 +10,8 @@ import com.appttude.h_mal.farmr.model.Sortable
interface Repository {
fun insertShiftIntoDatabase(shift: Shift): Boolean
fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean
fun readShiftsFromDatabase(): List<ShiftObject>?
fun readSingleShiftFromDatabase(id: Long): ShiftObject?
fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>>
fun readSingleShiftFromDatabase(id: Long): ShiftEntity?
fun deleteSingleShiftFromDatabase(id: Long): Boolean
fun deleteAllShiftsFromDatabase(): Boolean
fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?>

View File

@@ -1,50 +1,77 @@
package com.appttude.h_mal.farmr.data
import androidx.lifecycle.LiveData
import androidx.room.Room
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.Sortable
import com.appttude.h_mal.farmr.utils.dateStringIsValid
import com.appttude.h_mal.farmr.utils.timeStringIsValid
class RepositoryImpl(
private val legacyDatabase: LegacyDatabase,
roomDatabase: AppDatabase,
private val preferenceProvider: PreferenceProvider
): Repository {
private val shiftDao = roomDatabase.getShiftDao()
override fun insertShiftIntoDatabase(shift: Shift): Boolean {
return legacyDatabase.insertShiftDataIntoDatabase(shift) != null
val shiftEntity = shift.convertToShiftEntity()
return shiftDao.upsertFullShift(shiftEntity) > 0
}
override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean {
return legacyDatabase.updateShiftDataIntoDatabase(
id = id,
typeString = shift.type.type,
descriptionString = shift.description,
dateString = shift.date,
timeInString = shift.timeIn ?: "",
timeOutString = shift.timeOut ?: "",
duration = shift.duration ?: 0f,
breaks = shift.breakMins ?: 0,
units = shift.units ?: 0f,
payRate = shift.rateOfPay,
totalPay = shift.totalPay
) == 1
if (shift.description.isBlank() || shift.description.trim().length < 3) {
throw IllegalArgumentException("description required")
}
if (!shift.date.dateStringIsValid()) {
throw IllegalArgumentException("date required")
}
shift.timeIn?.takeIf { !it.timeStringIsValid() }?.let {
throw IllegalArgumentException("time in required")
}
shift.timeOut?.takeIf { !it.timeStringIsValid() }?.let {
throw IllegalArgumentException("time out required")
}
shift.breakMins?.takeIf { it < 0 }?.let {
throw IllegalArgumentException("break required")
}
if (shift.timeIn != null || shift.timeOut != null) {
if (shift.duration == null) throw IllegalArgumentException("Duration required")
}
shift.units?.takeIf { it < 0 }?.let {
throw IllegalArgumentException("Units required")
}
if (shift.rateOfPay < 0) {
throw IllegalArgumentException("Rate of pay required")
}
if (shift.totalPay < 0) {
throw IllegalArgumentException("Total pay required")
}
val shiftEntity = shift.convertToShiftEntity(id)
return shiftDao.upsertFullShift(shiftEntity) > 0
}
override fun readShiftsFromDatabase(): List<ShiftObject>? {
return legacyDatabase.readShiftsFromDatabase()
override fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>> {
return shiftDao.getAllFullShift()
}
override fun readSingleShiftFromDatabase(id: Long): ShiftObject? {
return legacyDatabase.readSingleShiftWithId(id)
override fun readSingleShiftFromDatabase(id: Long): ShiftEntity? {
return shiftDao.getCurrentFullShiftSingle(id)
}
override fun deleteSingleShiftFromDatabase(id: Long): Boolean {
return legacyDatabase.deleteSingleShift(id) == 1
return shiftDao.deleteShift(id) == 1
}
override fun deleteAllShiftsFromDatabase(): Boolean {
return legacyDatabase.deleteAllShiftsInDatabase() > 0
return shiftDao.deleteAllShifts() > 0
}
override fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?> {

View File

@@ -33,10 +33,10 @@ class ShiftsDbHelper(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAM
}
companion object {
private const val DATABASE_NAME = "shifts.db"
const val DATABASE_NAME = "shifts.db"
private const val DATABASE_VERSION = 4
private const val DEFAULT_TEXT = "Hourly"
private const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " ("
const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " ("
+ ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " TEXT NOT NULL, "
+ ShiftsEntry.COLUMN_SHIFT_DATE + " DATE NOT NULL, "
+ ShiftsEntry.COLUMN_SHIFT_TIME_IN + " TIME NOT NULL, "

View File

@@ -0,0 +1,51 @@
package com.appttude.h_mal.farmr.data.room
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
@Database(
entities = [ShiftEntity::class],
version = 5,
exportSchema = true
)
@TypeConverters(DateConverter::class, TimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun getShiftDao(): ShiftDao
companion object {
@Volatile
private var instance: AppDatabase? = null
private val LOCK = Any()
// create an instance of room database or use previously created instance
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also {
instance = it
}
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_4_5)
.addTypeConverter(DateConverter())
.addTypeConverter(TimeConverter())
.build()
}
}

View File

@@ -0,0 +1,37 @@
package com.appttude.h_mal.farmr.data.room
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.DeleteTable
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
@Dao
interface ShiftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertFullShift(item: ShiftEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertListOfFullShift(items: List<ShiftEntity>)
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
fun getCurrentFullShift(shiftId: Long): LiveData<ShiftEntity>
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
fun getCurrentFullShiftSingle(shiftId: Long): ShiftEntity?
@Query("SELECT * FROM shifts")
fun getAllFullShift(): LiveData<List<ShiftEntity>>
@Query("DELETE FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId")
fun deleteShift(shiftId: Long): Int
@Query("DELETE FROM shifts")
fun deleteAllShifts(): Int
}

View File

@@ -0,0 +1,19 @@
package com.appttude.h_mal.farmr.data.room.converters
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import java.sql.Date
@ProvidedTypeConverter
class DateConverter {
@TypeConverter
fun toDate(dateString: String): Date {
// Convert yyyy-MM-dd into date
return Date.valueOf(dateString)
}
@TypeConverter
fun fromDate(date: Date): String {
return date.toString()
}
}

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.farmr.data.room.converters
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import java.sql.Date
import java.sql.Time
import java.time.Instant
@ProvidedTypeConverter
class TimeConverter {
@TypeConverter
fun toTime(timeString: String?): Time {
// Convert HH:mm into date
timeString?.let { return Time.valueOf("$timeString:00") }
return Time.valueOf("00:00:00")
}
@TypeConverter
fun fromTime(time: Time): String {
// Return string in format of HH:mm
return time.toString().substring(0,5)
}
}

View File

@@ -0,0 +1,34 @@
package com.appttude.h_mal.farmr.data.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.TABLE_NAME
import com.appttude.h_mal.farmr.model.ShiftType
import java.sql.Date
import java.sql.Time
@Entity(tableName = TABLE_NAME)
data class ShiftEntity(
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) val description: String,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DATE, typeAffinity = ColumnInfo.UNDEFINED) val date: Date,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_IN, typeAffinity = ColumnInfo.TEXT) val timeIn: Time,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_OUT, typeAffinity = ColumnInfo.TEXT) val timeOut: Time,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_BREAK) val breakMins: Int? = 0,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DURATION, typeAffinity = ColumnInfo.REAL, defaultValue = "0") val duration: Float = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TYPE) val type: String,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_UNIT, typeAffinity = ColumnInfo.REAL) val units: Float? = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_PAYRATE, typeAffinity = ColumnInfo.REAL) val payRate: Float? = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TOTALPAY, typeAffinity = ColumnInfo.REAL) val totalPay: Float? = 0f,
@PrimaryKey
@ColumnInfo(name = ShiftsEntry._ID) val id: Long? = 0,
) {
fun convertToShiftObject(): ShiftObject {
return ShiftObject(
id ?: 0, type, description, date.toString(), timeIn.toString(), timeOut.toString(), duration, breakMins ?: 0, units ?: 0f, payRate ?: 0f, totalPay ?: 0f
)
}
}

View File

@@ -0,0 +1,14 @@
package com.appttude.h_mal.farmr.data.room.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS '" + ShiftsDbHelper.SQL_CREATE_PRODUCTS_TABLE_2 + "'");
database.execSQL("ALTER TABLE `shifts` ADD COLUMN `date` TEXT NOT NULL")
// CREATE TABLE IF NOT EXISTS (`description` TEXT NOT NULL, , `timein` TEXT NOT NULL, `timeout` TEXT NOT NULL, `break` INTEGER, `duration` REAL NOT NULL DEFAULT 0, `shifttype` TEXT NOT NULL, `unit` REAL, `payrate` REAL, `totalpay` REAL, `_id` INTEGER, PRIMARY KEY(`_id`))
}
}

View File

@@ -1,13 +1,15 @@
package com.appttude.h_mal.farmr.di
import androidx.room.RoomDatabase
import com.appttude.h_mal.farmr.base.BaseApplication
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
class ShiftApplication: BaseApplication() {
override fun createDatabase(): LegacyDatabase {
return LegacyDatabase(contentResolver)
override fun createDatabase(): AppDatabase {
return AppDatabase(this)
}
override fun createPrefs() = PreferenceProvider(this)

View File

@@ -1,11 +0,0 @@
package com.appttude.h_mal.farmr.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class EntityItem(
@PrimaryKey(autoGenerate = false)
val id: String,
val shift: Shift
)

View File

@@ -1,5 +1,8 @@
package com.appttude.h_mal.farmr.model
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.utils.calculateDuration
import com.appttude.h_mal.farmr.utils.formatToTwoDp
@@ -15,6 +18,39 @@ data class Shift(
val rateOfPay: Float,
val totalPay: Float
) {
fun convertToShiftEntity(): ShiftEntity {
val timeConverter = TimeConverter()
return ShiftEntity(
description = description,
type = type.type,
date = DateConverter().toDate(date),
timeIn = timeConverter.toTime(timeIn),
timeOut = timeConverter.toTime(timeOut),
duration = duration ?: 0f,
breakMins = breakMins ?: 0,
units = units ?: 0f,
payRate = rateOfPay,
totalPay = totalPay
)
}
fun convertToShiftEntity(id: Long): ShiftEntity {
val timeConverter = TimeConverter()
return ShiftEntity(
id = id,
description = description,
type = type.type,
date = DateConverter().toDate(date),
timeIn = timeConverter.toTime(timeIn),
timeOut = timeConverter.toTime(timeOut),
duration = duration ?: 0f,
breakMins = breakMins ?: 0,
units = units ?: 0f,
payRate = rateOfPay,
totalPay = totalPay
)
}
companion object {
// Invocation for Hourly
operator fun invoke(
@@ -60,6 +96,4 @@ data class Shift(
(units * rateOfPay).formatToTwoDp()
)
}
}

View File

@@ -13,6 +13,8 @@ import androidx.core.widget.doAfterTextChanged
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.base.BackPressedListener
import com.appttude.h_mal.farmr.base.BaseFragment
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.ID
@@ -31,6 +33,9 @@ import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item),
RadioGroup.OnCheckedChangeListener, BackPressedListener {
private val dateConverter = DateConverter()
private val timeConverter = TimeConverter()
private lateinit var mHourlyRadioButton: RadioButton
private lateinit var mPieceRadioButton: RadioButton
private lateinit var mLocationEditText: EditText
@@ -126,41 +131,41 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
// Since we are editing a shift lets load the shift data into the views
viewModel.getCurrentShift(arguments!!.getLong(ID))?.run {
mLocationEditText.setText(description)
mDateEditText.setText(date)
mDateEditText.setText(dateConverter.fromDate(date))
// Set types
mType = ShiftType.getEnumByType(type)
mDescription = description
mDate = date
mPayRate = rateOfPay
mDate = dateConverter.fromDate(date)
mPayRate = payRate!!
when (ShiftType.getEnumByType(type)) {
ShiftType.HOURLY -> {
mHourlyRadioButton.isChecked = true
mPieceRadioButton.isChecked = false
mTimeInEditText.setText(timeIn)
mTimeOutEditText.setText(timeOut)
mTimeInEditText.setText(timeConverter.fromTime(timeIn))
mTimeOutEditText.setText(timeConverter.fromTime(timeOut))
mBreakEditText.setText(breakMins.toString())
val durationText = "${duration.formatToTwoDpString()} Hours"
mDurationTextView.text = durationText
// Set fields
mTimeIn = timeIn
mTimeOut = timeOut
mTimeIn = timeConverter.fromTime(timeIn)
mTimeOut = timeConverter.fromTime(timeOut)
mBreaks = breakMins
}
ShiftType.PIECE -> {
mHourlyRadioButton.isChecked = false
mPieceRadioButton.isChecked = true
mUnitEditText.setText(units.formatToTwoDpString())
mUnitEditText.setText(units?.formatToTwoDpString())
// Set piece rate units
mUnits = units
}
}
mPayRateEditText.setText(rateOfPay.formatAsCurrencyString())
mTotalPayTextView.text = totalPay.formatAsCurrencyString()
mPayRateEditText.setText(payRate.formatAsCurrencyString())
mTotalPayTextView.text = totalPay?.formatAsCurrencyString()
calculateTotalPay()
}

View File

@@ -1,7 +1,6 @@
package com.appttude.h_mal.farmr.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
@@ -16,6 +15,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Sortable
@@ -37,25 +37,28 @@ class MainViewModel(
private val repository: Repository
) : ShiftViewModel(repository) {
private val _shiftLiveData = MutableLiveData<List<ShiftObject>>()
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
private val shiftLiveData: LiveData<List<ShiftEntity>> = repository.readShiftsFromDatabase()
private var mSort: Sortable = Sortable.ID
private var mOrder: Order = Order.ASCENDING
private val observer = Observer<List<ShiftObject>> {
it?.let {
val result = it.applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
private val observer = Observer<List<ShiftEntity>> {
it?.let { updateFiltrationAndPostResults(it) }
}
init {
// Load shifts into live data when view model has been instantiated
refreshLiveData()
shiftLiveData.observeForever(observer)
}
private fun updateFiltrationAndPostResults(entities: List<ShiftEntity>) {
val result = entities.mapToShiftObjects().applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
private fun List<ShiftEntity>.mapToShiftObjects(): List<ShiftObject> {
return map { i -> i.convertToShiftObject() }
}
private fun List<ShiftObject>.applyFilters(): List<ShiftObject> {
val filter = getFiltrationDetails()
@@ -75,14 +78,21 @@ class MainViewModel(
}
}
private fun comparedStringsContains(first: String?, second: String?): Boolean {
first?.let {
(second?.contains(it))?.let { c -> return c }
/*
* Check if string compareWith contains compareAgainst
*/
private fun comparedStringsContains(compareWith: String?, compareAgainst: String?): Boolean {
compareWith?.let {
(compareAgainst?.contains(it))?.let { c -> return c }
}
return comparedStrings(first, second)
return comparedStrings(compareWith, compareAgainst)
}
/*
* check if date [compareWith] fall between two dates
* if fromDate or toDate is null then it will take today's date for comparison
*/
private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? {
val first = fromDate?.convertDateString()
val second = toDate?.convertDateString()
@@ -96,7 +106,9 @@ class MainViewModel(
return compareDate.after(first) && compareDate.before(second)
}
/*
* When view-model is cleared we stop observing any livedata
*/
override fun onCleared() {
shiftLiveData.removeObserver(observer)
super.onCleared()
@@ -132,7 +144,7 @@ class MainViewModel(
var totalUnits = 0f
var totalPay = 0f
var lines = 0
_shiftLiveData.value?.applyFilters()?.forEach {
shiftLiveData.value?.mapToShiftObjects()?.applyFilters()?.forEach {
lines += 1
totalDuration += it.duration
when (ShiftType.getEnumByType(it.type)) {
@@ -156,16 +168,12 @@ class MainViewModel(
fun deleteShift(id: Long) {
if (!repository.deleteSingleShiftFromDatabase(id)) {
onError("Failed to delete shift")
} else {
refreshLiveData()
}
}
fun deleteAllShifts() {
if (!repository.deleteAllShiftsFromDatabase()) {
onError("Failed to delete all shifts from database")
} else {
refreshLiveData()
}
}
@@ -194,8 +202,12 @@ class MainViewModel(
return stringBuilder.toString()
}
/*
* After operations such as updating filtering or sorting
* we update the data we sent to the ui
*/
fun refreshLiveData() {
repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) }
shiftLiveData.value?.let { updateFiltrationAndPostResults(it) }
}
fun clearFilters() {
@@ -204,6 +216,9 @@ class MainViewModel(
refreshLiveData()
}
/*
* Build a .csv file to be exported
*/
fun createExcelSheet(file: File): File? {
val wbSettings = WorkbookSettings().apply {
locale = Locale("en", "EN")
@@ -232,7 +247,7 @@ class MainViewModel(
return null
}
val sortAndOrder = getSortAndOrder()
val data = shiftLiveData.value!!.applyFilters()
val data = shiftLiveData.value!!.mapToShiftObjects().applyFilters()
.sortList(sortAndOrder.first, sortAndOrder.second)
var currentRow = 0
val cells = data.map { shift ->

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.farmr.viewmodel
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success
@@ -17,6 +19,9 @@ class SubmissionViewModel(
private val repository: Repository
) : ShiftViewModel(repository) {
private val dateConverter = DateConverter()
private val timeConverter = TimeConverter()
fun insertHourlyShift(
description: String,
date: String,
@@ -26,7 +31,7 @@ class SubmissionViewModel(
breakMins: Int?,
) {
// Validate inputs from the edit texts
(description.length > 3).validateField {
(description.trim().length > 3).validateField {
onError("Description length should be longer")
return
}
@@ -53,7 +58,7 @@ class SubmissionViewModel(
val result = insertShiftIntoDatabase(
ShiftType.HOURLY,
description,
description.trim(),
date,
rateOfPay.formatToTwoDp(),
timeIn,
@@ -73,7 +78,7 @@ class SubmissionViewModel(
rateOfPay: Float
) {
// Validate inputs from the edit texts
(description.length > 3).validateField {
(description.trim().length > 3).validateField {
onError("Description length should be longer")
return
}
@@ -92,7 +97,7 @@ class SubmissionViewModel(
val result = insertShiftIntoDatabase(
type = ShiftType.PIECE,
description = description,
description = description.trim(),
date = date,
rateOfPay = rateOfPay.formatToTwoDp(),
null,
@@ -177,72 +182,72 @@ class SubmissionViewModel(
breakMins: Int? = null,
units: Float? = null,
): Boolean {
val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift()
val currentShift = repository.readSingleShiftFromDatabase(id)
?: throw IOException("Cannot update shift as it does not exist")
val mDate = date ?: dateConverter.fromDate(currentShift.date)
val mTimeIn = timeIn ?: timeConverter.fromTime(currentShift.timeIn)
val mTimeOut = timeOut ?: timeConverter.fromTime(currentShift.timeIn)
val shift = when (type) {
ShiftType.HOURLY -> {
// Shift type has changed so mandatory fields for hourly shift are now required as well
val insertTimeIn =
(timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted")
val insertTimeOut =
(timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
timeIn = insertTimeIn,
timeOut = insertTimeOut,
date = mDate,
timeIn = mTimeIn,
timeOut = mTimeOut,
breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
)
}
ShiftType.PIECE -> {
// Shift type has changed so mandatory fields for piece rate shift are now required as well
val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
units = insertUnits,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
date = mDate,
units = (units ?: currentShift.units) ?: 0f,
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
)
}
else -> {
if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) {
// Updates to description or date field
currentShift.copy(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
Shift(
ShiftType.getEnumByType(currentShift.type),
description ?: currentShift.description,
mDate,
mTimeIn,
mTimeOut,
currentShift.duration,
currentShift.breakMins,
currentShift.units,
currentShift.payRate ?: 0f,
currentShift.totalPay ?: 0f
)
} else {
// Updating shifts where shift type has remained the same
when (currentShift.type) {
when (ShiftType.getEnumByType(currentShift.type)) {
ShiftType.HOURLY -> {
val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException(
"No time in inserted"
)
val insertTimeOut = (timeOut ?: currentShift.timeOut)
?: throw IOException("No time out inserted")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
timeIn = insertTimeIn,
timeOut = insertTimeOut,
date = mDate,
timeIn = mTimeIn,
timeOut = mTimeOut,
breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
)
}
ShiftType.PIECE -> {
val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
units = insertUnits,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
date = mDate,
units = (insertUnits) ?: 0f,
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
)
}
}

View File

@@ -1,24 +1,38 @@
package com.appttude.h_mal.farmr.data
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.data.room.ShiftDao
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Shift
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.anyLong
import java.io.IOException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
class RepositoryImplTest {
@get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var repository: RepositoryImpl
@MockK
lateinit var db: LegacyDatabase
@RelaxedMockK
lateinit var db: AppDatabase
@MockK
lateinit var prefs: PreferenceProvider
@@ -32,20 +46,123 @@ class RepositoryImplTest {
@Test
fun readDatabase_validResponse() {
// Arrange
val elements = listOf<ShiftObject>(
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() }
)
val liveData = mockk<LiveData<List<ShiftEntity>>>()
//Act
every { db.readShiftsFromDatabase() } returns elements
every { db.getShiftDao().getAllFullShift() } returns liveData
// Assert
val result = repository.readShiftsFromDatabase()
assertIs<List<ShiftObject>>(result)
assertEquals(result.first().id, anyLong())
assertIs<LiveData<List<ShiftEntity>>>(result)
assertEquals(result, liveData)
}
@Test
fun updateShift_invalidDescription_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
val emptyDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "DQ "
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyDescExceptionReturned.message, "description required")
assertEquals(untrimmedDescExceptionReturned.message, "description required")
}
@Test
fun updateShift_invalidDate_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
every { shift.description } returns "Valid desc"
val emptyDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.date } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "2022-03-02 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "02-03-2020"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatDateExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "2022/03/02"
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyDateExceptionReturned.message, "date required")
assertEquals(untrimmedDateExceptionReturned.message, "date required")
assertEquals(wrongFormatDateExceptionReturned.message, "date required")
assertEquals(wrongFormatDateExceptionReturned2.message, "date required")
}
@Test
fun updateShift_invalidTimeInAndTimeOut_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
every { shift.description } returns "Valid desc"
every { shift.date } returns "2020-06-05"
val emptyTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "14:04 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "14 04"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeInExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "1404"
repository.updateShiftIntoDatabase(id, shift)
}
every { shift.timeIn } returns "14:00"
val emptyTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "14:04 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "14 04"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeOutExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "1404"
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyTimeInExceptionReturned.message, "time in required")
assertEquals(untrimmedTimeInExceptionReturned.message, "time in required")
assertEquals(wrongFormatTimeInExceptionReturned.message, "time in required")
assertEquals(wrongFormatTimeInExceptionReturned2.message, "time in required")
assertEquals(emptyTimeOutExceptionReturned.message, "time out required")
assertEquals(untrimmedTimeOutExceptionReturned.message, "time out required")
assertEquals(wrongFormatTimeOutExceptionReturned.message, "time out required")
assertEquals(wrongFormatTimeOutExceptionReturned2.message, "time out required")
}
}

View File

@@ -0,0 +1,26 @@
package com.appttude.h_mal.farmr.data.room.converters
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.sql.Time
class TimeConverterTest {
private val converter = TimeConverter()
@Test
fun toTime() {
val str = "16:04"
val time = converter.toTime(str)
assertEquals(time.toString(), "$str:00")
}
@Test
fun fromTime() {
val str = "16:04"
val time = converter.fromTime(Time.valueOf("16:04:00"))
assertEquals(time, str)
}
}

View File

@@ -2,7 +2,7 @@ package com.appttude.h_mal.farmr.utils
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.ShiftType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -42,108 +42,108 @@ fun sleep(millis: Long = 1000) {
}
fun getShifts() = listOf(
ShiftObject(
ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type,
ShiftEntity(
"Day one",
"2023-08-01",
"12:00",
"13:00",
1f,
ArgumentMatchers.anyInt(),
1f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(),
10f,
10f
),
ShiftObject(
10f,
ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type,
),
ShiftEntity(
"Day two",
"2023-08-02",
"12:00",
"13:00",
1f,
ArgumentMatchers.anyInt(),
1f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(),
10f,
10f
),
ShiftObject(
10f,
ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type,
),
ShiftEntity(
"Day three",
"2023-08-03",
"12:00",
"13:00",
1f,
"14:30",
30,
2f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(),
10f,
5f
),
ShiftObject(
20f,
ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type,
),
ShiftEntity(
"Day four",
"2023-08-04",
"12:00",
"13:00",
1f,
"14:30",
30,
2f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(),
10f,
5f
),
ShiftObject(
20f,
ArgumentMatchers.anyLong(),
ShiftType.PIECE.type,
),
ShiftEntity(
"Day five",
"2023-08-05",
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f,
10f,
10f
),
ShiftObject(
10f,
ArgumentMatchers.anyLong(),
ShiftType.PIECE.type,
),
ShiftEntity(
"Day six",
"2023-08-06",
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f,
10f,
10f
),
ShiftObject(
10f,
ArgumentMatchers.anyLong(),
ShiftType.PIECE.type,
),
ShiftEntity(
"Day seven",
"2023-08-07",
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f,
10f,
10f
),
ShiftObject(
10f,
ArgumentMatchers.anyLong(),
ShiftType.PIECE.type,
),
ShiftEntity(
"Day eight",
"2023-08-08",
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f,
10f,
10f
10f,
ArgumentMatchers.anyLong(),
),
)

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.farmr.viewmodel
import android.os.Bundle
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.utils.ID
import io.mockk.every
import io.mockk.mockk
@@ -16,7 +17,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
fun retrieveData_validBundleAndId_successfulRetrieval() {
// Arrange
val id = anyLong()
val shift = mockk<ShiftObject>()
val shift = mockk<ShiftEntity>()
val bundle = mockk<Bundle>()
// Act
@@ -25,7 +26,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
viewModel.retrieveData(bundle)
// Assert
assertIs<ShiftObject>(retrieveCurrentData())
assertIs<ShiftEntity>(retrieveCurrentData())
assertEquals(
retrieveCurrentData(),
shift
@@ -36,7 +37,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() {
// Arrange
val id = anyLong()
val shift = mockk<ShiftObject>()
val shift = mockk<ShiftEntity>()
val bundle = mockk<Bundle>()
// Act

View File

@@ -1,17 +1,23 @@
package com.appttude.h_mal.farmr.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.prefs.DATE_IN
import com.appttude.h_mal.farmr.data.prefs.DATE_OUT
import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
import com.appttude.h_mal.farmr.data.prefs.TYPE
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.ViewState
import com.appttude.h_mal.farmr.utils.getOrAwaitValue
import com.appttude.h_mal.farmr.utils.getShifts
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -31,7 +37,10 @@ class MainViewModelTest {
@Before
fun setUp() {
repository = mockk()
every { repository.readShiftsFromDatabase() }.returns(null)
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
every { repository.readShiftsFromDatabase() }.returns(liveData)
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter())
viewModel = MainViewModel(repository)
}
@@ -45,47 +54,60 @@ class MainViewModelTest {
@Test
fun getShiftsFromRepository_liveDataIsShown() {
// Arrange
val listOfShifts = anyList<ShiftObject>()
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = anyList<ShiftEntity>()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
// Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
every { repository.readShiftsFromDatabase() }.returns(liveData)
viewModel = MainViewModel(repository)
viewModel.refreshLiveData()
// Assert
assertEquals(retrieveCurrentData(), listOfShifts)
assertEquals(retrieveCurrentData(), shifts)
}
@Test
fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() {
// Arrange
val listOfShifts = getShifts()
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = getShifts()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
// Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
every { repository.readShiftsFromDatabase() }.returns(liveData)
viewModel = MainViewModel(repository)
viewModel.refreshLiveData()
val retrievedShifts = retrieveCurrentData()
val description = viewModel.getInformation()
// Assert
assertEquals(retrievedShifts, listOfShifts)
assertEquals(retrievedShifts, shifts.map { it.convertToShiftObject() })
assertEquals(
description, "8 Shifts\n" +
" (4 Hourly/4 Piece Rate)\n" +
"Total Hours: 4.0\n" +
"Total Hours: 6.0\n" +
"Total Units: 4.0\n" +
"Total Pay: £70.00"
"Total Pay: £100.00"
)
}
@Test
fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() {
// Arrange
val listOfShifts = getShifts()
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = getShifts()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type }
// Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
every { repository.readShiftsFromDatabase() }.returns(liveData)
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type))
viewModel = MainViewModel(repository)
viewModel.refreshLiveData()
val retrievedShifts = retrieveCurrentData()
val description = viewModel.getInformation()
@@ -96,18 +118,18 @@ class MainViewModelTest {
val descriptionAfterClearedFilter = viewModel.getInformation()
// Assert
assertEquals(retrievedShifts, filteredShifts)
assertEquals(retrievedShifts, filteredShifts.map { it.convertToShiftObject() })
assertEquals(
description, "4 Shifts\n" +
"Total Hours: 4.0\n" +
"Total Pay: £30.00"
"Total Hours: 6.0\n" +
"Total Pay: £60.00"
)
assertEquals(
descriptionAfterClearedFilter, "8 Shifts\n" +
" (4 Hourly/4 Piece Rate)\n" +
"Total Hours: 4.0\n" +
"Total Hours: 6.0\n" +
"Total Units: 4.0\n" +
"Total Pay: £70.00"
"Total Pay: £100.00"
)
}