5

How can I test Transaction database operation, insertItineraries for Room?

Error

java.lang.IllegalStateException: This job has not completed yet

at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1188) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45) at com.andigeeky.skyscannertest.db.ItineraryDaoTest.test insert itineraries with legs(ItineraryDaoTest.kt:44) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

ItineraryDao

@Dao
@OpenForTesting
interface ItineraryDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertLegEntities(legs: List<LegEntity>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertItineraryEntities(itineraries: List<ItineraryEntity>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertItineraryLeg(itineraryLegEntities: List<ItineraryLegEntity>)

    @Transaction
    suspend fun insertItineraries(
        itineraries: List<ItineraryEntity>,
        legs: List<LegEntity>,
        itineraryLegEntities: List<ItineraryLegEntity>
    ){
        insertItineraryEntities(itineraries)
        insertLegEntities(legs)
        insertItineraryLeg(itineraryLegEntities)
    }

    @Transaction
    @Query("SELECT * FROM ItineraryEntity")
    fun getItineraryWithLegs(): LiveData<List<ItineraryWithLegs>>
}

ItineraryDaoTest

@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class ItineraryDaoTest : DbTest() {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    @Captor
    lateinit var captor: ArgumentCaptor<ArrayList<ItineraryWithLegs>>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun `test insert itineraries with legs`() {
        runBlockingTest {
            val observer = mock<Observer<List<ItineraryWithLegs>>>()
            val legs = TestUtil.createLegs(1)
            val itineraries = TestUtil.createItineraries(1)
            val itineraryLegs = TestUtil.createItineraryLegEntities(1)

            skyScannerDatabase.itineraryDao().insertItineraries(itineraries, legs, itineraryLegs)
            skyScannerDatabase.itineraryDao().getItineraryWithLegs().observeForever(observer)

            captor.run {
                verify(observer, times(1)).onChanged(capture())
                assertEquals(itineraryLegs.size, value.size)
            }
        }
    }
}

DbTest

@UseExperimental(ExperimentalCoroutinesApi::class)
abstract class DbTest : CoroutineTestBase() {
    @Rule
    @JvmField
    val countingTaskExecutorRule = CountingTaskExecutorRule()
    lateinit var skyScannerDatabase: SkyScannerDatabase

    @Before
    fun initDb() {
        val app = ApplicationProvider.getApplicationContext<Context>()
        skyScannerDatabase = Room.inMemoryDatabaseBuilder(app, SkyScannerDatabase::class.java)
            .allowMainThreadQueries()
            .setTransactionExecutor(Executors.newSingleThreadExecutor())
            .build()
    }

    @After
    fun closeDb() {
        countingTaskExecutorRule.drainTasks(10, TimeUnit.SECONDS)
        skyScannerDatabase.close()
    }
}
AndiGeeky
  • 11,266
  • 6
  • 50
  • 66
  • Out of curiosity, why is this a Robolectric test and not an Espresso test? – Emmanuel Jan 27 '20 at 13:45
  • Running it as `unit test`! :thinking_face – AndiGeeky Jan 27 '20 at 17:10
  • 1
    RIght, I typically see db tests as on device tests. Doing so you could use `AsyncTask.THREAD_POOL_EXECUTOR` which `Espresso` monitors by default. I am thinking that the problem you are seeing comes from `setTransactionExecutor(Executors.newSingleThreadExecutor())`. Using Espresso and this approach avoids having to use an `IdlingResource`. – Emmanuel Jan 27 '20 at 17:39
  • @Emmanuel Let me try that! Thanks :) – AndiGeeky Jan 28 '20 at 09:55
  • 1
    The error essentially means that a child coroutine was launched in another scope, and it has not completed at the time `runBlockingTest` did. When building your Room DB set the query executor to a test coroutine dispatcher. `val dispatcher = TestCoroutineDispatcher()` then in your DB init code, `setQueryExecutor(dispatcher.asExecutor())`. Most tests work fine with this, but I'm currently stuck with an issue with deadlocked transactions. See https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/ – themoondothshine May 30 '21 at 16:22
  • `setTransactionExecutor(Executors.newSingleThreadExecutor())` worked for me, with Robolectric too, but I'm not using `runBlockingTest`, I'm using standard `runBlocking`. And also I'm not using LIveData, just suspend functions to read. Just in case it helps anyone. – ianribas Jun 15 '21 at 14:41

1 Answers1

0

You should test your DAO using @RunWith(AndroidJUnit4::class) annotation on top of your test class. Running as an instrumentation test (tests classes under androidTest folder).

@RunWith(AndroidJUnit4::class)
class YourTestClass {
  @get:Rule
  var instantTaskExecutorRule = InstantTaskExecutorRule()
  lateinit var skyScannerDatabase: SkyScannerDatabase

  @Before
  fun initDb() {
    val app = ApplicationProvider.getApplicationContext<Context>()
    skyScannerDatabase = Room.inMemoryDatabaseBuilder(app, SkyScannerDatabase::class.java)
        .allowMainThreadQueries()
        .setTransactionExecutor(Executors.newSingleThreadExecutor())
        .build()
  }

  @After
  fun closeDb() {
    skyScannerDatabase.close()
  }

  // ...

  @Test
  fun testMethod() = runBlockingTest {
    skyScannerDatabase.itineraryDao().insertItineraries(...)
  }
}

You should have the following configuration on your build.gradle module.

android {
  // ...

  defaultConfig {
    // ...
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    // ...
  }
}

// kotlin_coroutines_version = '1.3.3'
// test_arch_core_testing = '2.1.0'
// test_ext_junit_version = '1.1.1'
// test_runner_version = '1.2.0'

dependencies {
  androidTestImplementation "androidx.arch.core:core-testing:$test_arch_core_testing"   
  androidTestImplementation "androidx.test.ext:junit:$test_ext_junit_version"   
  androidTestImplementation "androidx.test:runner:$test_runner_version"  
  androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version"
}

Take a look on this post.

heronsanches
  • 524
  • 7
  • 12