Search code examples
kotlinintegration-testingandroid-roomkotlin-coroutines

How to implement sorting in Room and how do I test it?


I'm currently trying to implement sorting in Room. Here's my code:

Dao

@Query("SELECT * FROM model ORDER BY " +
        "CASE WHEN :isAsc = true THEN :field END ASC, " +
        "CASE WHEN :isAsc = false THEN :field END DESC ")
fun sortList(field: String, isAsc: Boolean?): Flow<List<Model>>

Repo

@WorkerThread
suspend fun sort(field: String, isAsc: Boolean?):  Flow<List<Model>>{
    return dao.sortList(field, isAsc)
}

ViewModel

fun sort(field: String, isAscending: Boolean? = true) = viewModelScope.launch {
    repo.sort(field, isAscending)
}

As a side question, is my code possible? Can I make the order by field a parameter as well? I want to have only one function for sorting, if possible.

This is how I currently test it.

dao.insert(instance1)
//repeat this a couple of times

val tempLst = arrayListOf<Model>()
tempLst.add(instance1)
//repeat this the same times as the dao.insert

val sortedLst = tempLst.sortedBy { it.schedDate }

val latch = CountDownLatch(1)
val job = async(Dispatchers.IO) {
    dao.sortList("schedule_date", true).collect {
        assertThat(sortedLst).isEqualTo(it)
        latch.countDown()
    }
}
latch.await()
job.cancelAndJoin()

Now, I searched for how to test sorting but can't seem to find any. Is sorting testable? How do you test it? Also, for some reason, that test doesn't finish. I hope you can help me with that as well.


Solution

  • Is sorting testable?

    Yes see below.

    Can I make the order by field a parameter as well?

    Yes, BUT see below.

    How do you test it?

    as below,

    Also, for some reason, that test doesn't finish.

    your issue is probably because it is running an another thread. again see below

    How I have tested you actual query:-

    First created the DB code NOTE using .allowMainThreadQueries AND without Flow as per :-

    @Entity
    data class Model(
        @PrimaryKey
        val modelId: Long?=null,
        val col1: String,
        val col2: String,
        val col3: String
    )
    
    @Dao
    interface TheDAOs {
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(model: Model): Long
    
        @Query("SELECT * FROM model ORDER BY " +
                "CASE WHEN :isAsc THEN :field END ASC, " +
                "CASE WHEN :isAsc THEN :field END DESC ")
        fun sortList(field: String, isAsc: Boolean?): /*Flow<*/List<Model>/*>*/
    }
    

    :-

    @Database(entities = [Model::class], exportSchema = false, version = 1)
    abstract class TheDatabase: RoomDatabase() {
        abstract fun getTheDAOs(): TheDAOs
    
        companion object {
            private var instance: TheDatabase?=null
            fun getInstance(context: Context): TheDatabase {
                if (instance==null) {
                    instance=Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                        .allowMainThreadQueries() /*<<<<<<<<<< FOR TESTING >>>>>>>>>>*/
                        .build()
                }
                return instance as TheDatabase
            }
        }
    }
    

    Then in an activity, coded:-

    class MainActivity : AppCompatActivity() {
        lateinit var db: TheDatabase
        lateinit var dao: TheDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = TheDatabase.getInstance(this)
            dao = db.getTheDAOs()
    
            dao.insert(Model(modelId = 10, col1 = "ABC", col2="MNO", col3 = "XYZ"))
            dao.insert(Model(modelId = 20, col1 = "XYZ", col2="ABC", col3 = "MNO"))
            dao.insert(Model(modelId = 30, col1 = "MNO", col2="XYZ", col3 = "ABC"))
    
            for (m in dao.sortList("col1",true)) {
                Log.d("DBSORT_RUN1","ID is ${m.modelId} COL1=${m.col1} COL2=${m.col2} COL3=${m.col3}")
            }
            for (m in dao.sortList("col2",true)) {
                Log.d("DBSORT_RUN2","ID is ${m.modelId} COL1=${m.col1} COL2=${m.col2} COL3=${m.col3}")
            }
            for (m in dao.sortList("col1",false)) {
                Log.d("DBSORT_RUN3","ID is ${m.modelId} COL1=${m.col1} COL2=${m.col2} COL3=${m.col3}")
            }
        }
    }
    

    The result written to the log:-

    2023-03-22 21:10:34.027 D/DBSORT_RUN1: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 21:10:34.027 D/DBSORT_RUN1: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 21:10:34.027 D/DBSORT_RUN1: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    
    
    2023-03-22 21:10:34.030 D/DBSORT_RUN2: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 21:10:34.030 D/DBSORT_RUN2: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 21:10:34.030 D/DBSORT_RUN2: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    
    
    2023-03-22 21:10:34.033 D/DBSORT_RUN3: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 21:10:34.033 D/DBSORT_RUN3: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 21:10:34.033 D/DBSORT_RUN3: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    

    They have not been sorted as expected (they are all the same)

    However, perhaps consider:-

    @Query("SELECT * FROM model ORDER BY  " +
            "CASE WHEN :isAsc AND :field='col1' THEN col1 END ASC, "
            + "CASE WHEN NOT :isAsc AND :field='col1' THEN col1 END DESC, "
            + "CASE WHEN :isAsc AND :field='col2' THEN col2 END ASC, "
            + "CASE WHEN NOT :isAsc AND :field='col2' THEN col1 END DESC, "
            + "CASE WHEN :isAsc AND :field='col3' THEN col3 END ASC, "
            + "CASE WHEN NOT :isAsc AND :field='col3' THEN col3 END DESC "
    )
    fun sortListV2(field: String, isAsc: Boolean?): /*Flow<*/List<Model>/*>*/
    

    Resulting in (when sortListV2 is used):-

    2023-03-22 21:20:49.457 D/DBSORT_RUN1: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 21:20:49.457 D/DBSORT_RUN1: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    2023-03-22 21:20:49.457 D/DBSORT_RUN1: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    
    
    2023-03-22 21:20:49.460 D/DBSORT_RUN2: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 21:20:49.460 D/DBSORT_RUN2: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 21:20:49.460 D/DBSORT_RUN2: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    
    
    2023-03-22 21:20:49.462 D/DBSORT_RUN3: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 21:20:49.462 D/DBSORT_RUN3: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    2023-03-22 21:20:49.462 D/DBSORT_RUN3: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    

    the expected results

    An alternative is to utilise an @RawQuery, which can be simpler BUT is not checked at compile time e.g.

    @RawQuery
    fun sortListRawQuery(query: SimpleSQLiteQuery): List<Model>
    fun sortListV3(field: String, isAsc: Boolean): List<Model> {
        var ascDesc = "ASC"
        if (!isAsc) ascDesc = "DESC"
        return sortListRawQuery(SimpleSQLiteQuery("SELECT * FROM model ORDER BY ${field} ${ascDesc};"))
    }
    

    And using sortListV3 then :-

    2023-03-22 22:01:14.133  D/DBSORT_RUN1: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 22:01:14.133  D/DBSORT_RUN1: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    2023-03-22 22:01:14.133  D/DBSORT_RUN1: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    
    
    2023-03-22 22:01:14.135  D/DBSORT_RUN2: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 22:01:14.135  D/DBSORT_RUN2: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    2023-03-22 22:01:14.135  D/DBSORT_RUN2: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    
    
    2023-03-22 22:01:14.138  D/DBSORT_RUN3: ID is 20 COL1=XYZ COL2=ABC COL3=MNO
    2023-03-22 22:01:14.138  D/DBSORT_RUN3: ID is 30 COL1=MNO COL2=XYZ COL3=ABC
    2023-03-22 22:01:14.138  D/DBSORT_RUN3: ID is 10 COL1=ABC COL2=MNO COL3=XYZ
    
    • Of course you can wrap the results in Flows as required.