Android Dev / / 2022. 11. 28. 11:21

Firebase - Room synchronizable network + AndroidTest 예제

 

1. 해당 케이스의 테스트는 mocked networking이 아니라 actual networking using Firebase을 통해 테스트하는 예제이다.

따라서 firebase에 대한 connecting(by json)이 되어 있어야 하며, room 또한 mocked room by array가 아니라 androidTest를 통해 실제 database 를 temporary instantiating 하여 사용한다.

 

basically configuration of Data layer는 NiA google sample app을 참고하고 있으며, 아래의 링크를 참고하면 된다.(https://witcheryoon.tistory.com/343

 

Scenario of example test case flow

 

2. 해당 시나리오는 다음과 같다. 

 

아래와 같은 Entity를 구성하고, Network datamodel에서 synced라는 요소를 추가하여 synchronizable한 상태를 체크할 것이다.

data class BulletinEntity(
//    @PrimaryKey(autoGenerate = true)
//    val id: Long? = 0,
    @PrimaryKey
    @ColumnInfo(defaultValue = "")
    val fid: String,
    @ColumnInfo(defaultValue = "")
    val images: String, //["images/125.jpg","images/248.jpg","images/224.jpg","images/038.jpg"] 의 형태로 저장
    @ColumnInfo(defaultValue = "")
    val score: String,
    val date: Date,
    @ColumnInfo(defaultValue = "")
    val name : String,
    @ColumnInfo(defaultValue = "")
    val text : String,
    @ColumnInfo(defaultValue = "")
    val title : String,
    var synced : Boolean = false
)

 

여기서 synced가 true가 되지 않은 대상에 대해 모두 firebase로 전송하여 remote와의 동기화를 시키는 전략을 택하였고, 여기서는 구현되어 있지않지만 synced false가 있다면 적당한 시간 주기로 polling하여 syncing 시키는 (위의 이미지에서 괄호로 되어있는 부분) 메소드를 만들 수 있을 것이다.

 

 

3. dao와 db에 대한 구현은 다 되어 있다고 가정하고  androidTest에서 구현한 full code는 아래와 같다.

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.vlmplayground.core.data.repository.OfflineFirstBulletinRepository
import com.vlmplayground.core.database.VlmDatabase
import com.vlmplayground.core.database.dao.BulletinBoardDao
import com.vlmplayground.core.database.model.BulletinEntity
import com.vlmplayground.core.database.model.asDataModel
import com.vlmplayground.core.network.firebase.FirebaseNetworkDataSource
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import java.util.*

class ActualDBtest {

    private lateinit var bulletinBoardDao: BulletinBoardDao
    private lateinit var db: VlmDatabase

    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(
            context,
            VlmDatabase::class.java
        ).build()
        bulletinBoardDao = db.bulletinDao()
    }

    @Test
    fun bulletinDaoInsertingTest() = runTest {

//        val bulletinBoardDao = TestBulletinBoardDao() <= replaced by Actual DB
        val networkDataSource = FirebaseNetworkDataSource(Firebase.firestore)
        val bulletinRepository = OfflineFirstBulletinRepository(
            bulletinBoardDao = bulletinBoardDao,
            firebaseNetworkDataSource = networkDataSource
        )

        val bulletinList = listOf(
            BulletinEntity(
                fid = "1111testableInsertingSet1",
                images = "images/076.jpg,images/131.jpg,images/015.jpg,images/253.jpg,images/044.jpg,images/019.jpg",
                score = "111",
                date = Date(1669021905482),
                name = "침착한 황소대가리",
                text = "천하를 실로 넣는 긴지라 하는 교향악이다. 봄날의 들어 온갖 이상을 그러므로 이것은 피다.",
                title = "지혜는 뜨거운지라, 거선의 풀이 있다. 그들은 찾아 굳세게 불어 이것이야말로 쓸쓸하랴? 이상은 지혜는 보배를 거친 생명을 열락의 약동하다. 이는 무엇을 있으며, 살았으며, 예가 길을 사는가 영락과 부패뿐이다. 위하여 힘차게 대한 끓는다. 바로 있을 청춘 생생하며, 부패를 힘있다. 굳세게 품었기 대중을 풀이 아니더면, 못할 것이다. 커다란 능히 충분히 무엇을 이상, 그들의 그들은 구할 무한한 있으랴?	불러 이름과, 오면 자랑처럼 별빛이 버리었습니다. 멀리 된 것은 별 가을 위에 라이너 않은 까닭입니다."
            ),
            BulletinEntity(
                fid = "1111testableInsertingSet2",
                images = "images/003.jpg,images/039.jpg,images/195.jpg,images/222.jpg,images/191.jpg,images/025.jpg",
                score = "222",
                date = Date(1669026585477),
                name = "우월한 차이니스",
                text = "위에 덮어 파란 말 피어나듯이 노새, 봅니다.",
                title = "이름을 나는 묻힌 시와 가슴속에 봄이 지나고 다하지 위에도 봅니다. 청춘이 한 별에도 아직 다하지 너무나 이름과 듯합니다. 않은 것은 별 이네들은 북간도에 계십니다. 언덕 아름다운 하나에 라이너 버리었습니다. 사랑과 별 지나고 별을 소녀들의 가득 까닭입니다.	까닭이요, 풀이 별들을 까닭입니다. 밤을 남은 너무나 묻힌 지나고 풀이 그리워 써 봅니다. 별 가을 딴은 있습니다. 내 아직 오는 봅니다."
            )
        )

        bulletinBoardDao.insertOrIgnoreBulletins(bulletinList)

        runBlocking {

            bulletinRepository.uploadToBulletinBoard(
                BulletinEntity(
                    fid = "1111testableInsertingSet3",
                    images = "images/001.jpg,images/002.jpg,images/003.jpg",
                    score = "111",
                    date = Date(1669066725481),
                    name = "테스트 닉네임",
                    text = "평화스러운 얼음이 천지는 되는 하여도 있는 사막이다.",
                    title = " 풍부하게 피가 뛰노는 천자만홍이 가슴이 지혜는 속잎나고, 것이다. 용감하고 작고 뿐이다. 원질이 무한한 대한 품으며,지혜는 무엇을 작고 온갖 못할 낙원을 힘차게 있는 사막이다. 것은 바이며, 두기 얼마나 우리의 우리는 튼튼하며, 불러 피다. 봄바람을 커다란 가는 쓸쓸하랴? 인간에 힘차게 그들의 쓸쓸한 예가 아름답고 없으면, 고행을 돋고, 있으랴? 하는 위하여 굳세게 철환하였는가? 하였으며, 때에, 석가는 없는 싶이 고행을 가진 불어 것이다. 심장은 피고 청춘의 피가 넣는 청춘은 밝은 쓸쓸하랴? 것은 품었기 구하지 미묘한 청춘의 그들에게 장식하는 생생하며, 뭇 것이다.	아직 헤일 이름을 이런 어머니, 사랑과 봅니다. 이름과, 나는 노루, 딴은 하나에 어머니, 당신은 토끼, 버리었습니다. 많은 위에도 불러 이름과, 있습니다.",
                ).asDataModel()
            )

        }

    }

}

 

4. @Before block에서 room DB를 임시로 instance하고 dao를 연결한다.

그뒤 테스트 firebase에 대한 network source를 그대로 불러와서 인스턴스한다.

 

아래는 FirebaseNetworkDataSource를 구성하는 코드의 일부이다.

 

1> interface

interface VlmPlaygroundDataSource {

    fun getBulletinByTimestamp(movePoint: Long = 0L): Flow<List<NetworkBulletin>>

    fun deleteBulletin(fid : String)

    suspend fun insertOrIgnoreBulletins(entities: List<NetworkBulletin>) : Boolean

}

 

2> Datasource

import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.DocumentChange
import com.google.firebase.firestore.FirebaseFirestore
import com.vlmplayground.core.network.VlmPlaygroundDataSource
import com.vlmplayground.core.network.model.NetworkBulletin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.tasks.await
import java.util.*
import javax.inject.Inject

class FirebaseNetworkDataSource @Inject constructor(private val firebase : FirebaseFirestore)
    : VlmPlaygroundDataSource {

    //collectionRef.where("startTime", ">=", "1506816000").where("startTime", "<=", "1507593600")
    override fun getBulletinByTimestamp(movePoint: Long): Flow<List<NetworkBulletin>> = callbackFlow {
        ...
      }

    override suspend fun insertOrIgnoreBulletins(entities: List<NetworkBulletin>) : Boolean {

        var result = false
        var eventsCollection: CollectionReference? = null
        try
        {
            eventsCollection = firebase.collection("bulletinBoard")
        }
        catch (e: Throwable)
        {
            return result
        }

        firebase.runBatch{ batch ->
            if(entities.size > 500) { //firebase runbatch is only allowed until size 500
                entities.subList(0,500)
            }
            else {
                entities
            }
            .forEach { aEntry -> //the reason why under 500, because of firestore limitation
                val documentPath = eventsCollection.document(aEntry.fid)
                documentPath.let { batch.set(it, aEntry) }
            }
        }.addOnCompleteListener { batchResponse ->
            result = batchResponse.isSuccessful
        }.await()

        return result
    }

    override fun deleteBulletin(fid: String) {
        ...
    }



}

3> OfflineFirstBulletin - Repository에 대한 구성 예제

 

1) 위 flow의 묘사대로 upload플로우를 구성해놓았다.

class OfflineFirstBulletinRepository @Inject constructor(
    private val bulletinBoardDao: BulletinBoardDao,
    private val firebaseNetworkDataSource : FirebaseNetworkDataSource,
) : BulletinRepository {

    override fun syncBulletinBoard(): Flow<Unit> = ...

    //upload
    override suspend fun uploadToBulletinBoard(newBulletin : Bulletin) {
        bulletinBoardDao.insertOrIgnoreBulletin(newBulletin.asEntity())
        val getBulletinList: List<BulletinEntity> = bulletinBoardDao.getUnsyncedBulletin().first()

        val insertingResult = firebaseNetworkDataSource.insertOrIgnoreBulletins(
            entities = getBulletinList.map{it.asDataModel().asNetworkBulletin()}
        )

        if(insertingResult)
        {
            getBulletinList.forEach { it.synced = true }
        }

        bulletinBoardDao.updateBulletin(entities = getBulletinList)
        
    }
}

 

2) Dao 

@Dao
interface BulletinBoardDao {

    @Query(
        value = """
        SELECT * FROM bulletinboard
        """
    )
    fun getAllEntityStream(): Flow<List<BulletinEntity>>

    @Query(
        value = """
            DELETE FROM bulletinboard
        """
    )
    suspend fun deleteAllEntityStream()

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertOrIgnoreBulletin(bulletinEntity : BulletinEntity): Long

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertOrIgnoreBulletins(bulletinEntities: List<BulletinEntity>): List<Long>

    @Update
    suspend fun updateBulletin(entities: List<BulletinEntity>)

    @Upsert
    suspend fun upsertBulletin(entities: List<BulletinEntity>)

    @Query(
        value = """
            Select * From bulletinboard where synced = false
        """
    )
    fun getUnsyncedBulletin():Flow<List<BulletinEntity>>

}

 

해당 구성을 통해 firebase network를 그대로 이용한 Testable model을 generating할 수 있다.

 

 

아래는 실행예제에 대한 영상입니다.

https://youtu.be/uNTouQvHkIs

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유