Simplify running suspending tests (#1452)

* Move LoggerModule to di package and add TestLoggerModule

* Remove main dispatcher injection and use runTest directly

* Add verbose logging module for tests

* Add test for UnifiedPushService.onUnregistered
This commit is contained in:
Ricki Hirner
2025-05-09 14:55:30 +02:00
committed by GitHub
parent 5f647b7403
commit 47afddbd08
8 changed files with 60 additions and 40 deletions

View File

@@ -9,8 +9,12 @@ import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.test.runner.AndroidJUnitRunner
import at.bitfire.davdroid.di.TestCoroutineDispatchersModule
import at.bitfire.davdroid.sync.SyncAdapterService
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.setMain
@Suppress("unused")
class HiltTestRunner : AndroidJUnitRunner() {
@@ -18,6 +22,7 @@ class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, name: String, context: Context): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreate(arguments: Bundle?) {
super.onCreate(arguments)
@@ -27,6 +32,9 @@ class HiltTestRunner : AndroidJUnitRunner() {
// disable sync adapters
SyncAdapterService.syncActive.set(false)
// set main dispatcher for tests (especially runTest)
Dispatchers.setMain(TestCoroutineDispatchersModule.standardTestDispatcher)
}
}

View File

@@ -4,6 +4,7 @@
package at.bitfire.davdroid.di
import at.bitfire.davdroid.di.TestCoroutineDispatchersModule.standardTestDispatcher
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
@@ -13,8 +14,10 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
/**
* If you run tests that switch context to one of these dispatchers, use `runTest(mainDispatcher)`
* with `mainDispatcher` being an injected [MainDispatcher] instead of plain `runTest`.
* Provides test dispatchers to be injected instead of the normal ones.
*
* The [standardTestDispatcher] is set as main dispatcher in [at.bitfire.davdroid.HiltTestRunner],
* so that tests can just use [kotlinx.coroutines.test.runTest] without providing [standardTestDispatcher].
*/
@Module
@TestInstallIn(

View File

@@ -0,0 +1,33 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.di
import at.bitfire.davdroid.log.LogcatHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Singleton
/**
* Module that provides verbose logging for tests.
*/
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [LoggerModule::class]
)
@Module
class TestLoggerModule {
@Provides
@Singleton
fun logger(): Logger = Logger.getGlobal().apply {
level = Level.ALL
addHandler(LogcatHandler())
}
}

View File

@@ -8,7 +8,6 @@ import android.content.Context
import android.content.Intent
import android.os.IBinder
import androidx.test.rule.ServiceTestRule
import at.bitfire.davdroid.di.MainDispatcher
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
@@ -19,7 +18,6 @@ import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit4.MockKRule
import io.mockk.mockk
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@@ -48,10 +46,6 @@ class UnifiedPushServiceTest {
@ApplicationContext
lateinit var context: Context
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@RelaxedMockK
@BindValue
lateinit var pushRegistrationManager: PushRegistrationManager
@@ -70,7 +64,7 @@ class UnifiedPushServiceTest {
@Test
fun testOnNewEndpoint() = runTest(mainDispatcher) {
fun testOnNewEndpoint() = runTest {
val endpoint = mockk<PushEndpoint> {
every { url } returns "https://example.com/12"
}
@@ -84,7 +78,7 @@ class UnifiedPushServiceTest {
}
@Test
fun testOnRegistrationFailed() = runTest(mainDispatcher) {
fun testOnRegistrationFailed() = runTest {
unifiedPushService.onRegistrationFailed(FailedReason.INTERNAL_ERROR, "34")
advanceUntilIdle()
@@ -95,8 +89,8 @@ class UnifiedPushServiceTest {
}
@Test
fun testOnUnregistered() = runTest(mainDispatcher) {
unifiedPushService.onRegistrationFailed(FailedReason.INTERNAL_ERROR, "45")
fun testOnUnregistered() = runTest {
unifiedPushService.onUnregistered("45")
advanceUntilIdle()
coVerify {

View File

@@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.di.MainDispatcher
import at.bitfire.davdroid.push.PushRegistrationManager
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
@@ -17,7 +16,6 @@ import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.coVerify
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit4.MockKRule
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@@ -48,10 +46,6 @@ class CollectionSelectedUseCaseTest {
@Inject
lateinit var collectionRepository: DavCollectionRepository
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
val service = Service(
id = 1,
type = Service.Companion.TYPE_CALDAV,
@@ -87,7 +81,7 @@ class CollectionSelectedUseCaseTest {
@Test
fun testHandleWithDelay() = runTest(mainDispatcher) {
fun testHandleWithDelay() = runTest {
useCase.handleWithDelay(collectionId = collection.id)
advanceUntilIdle()

View File

@@ -9,13 +9,11 @@ import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.di.MainDispatcher
import at.bitfire.davdroid.network.HttpClient
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.junit4.MockKRule
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.runTest
import okhttp3.CookieJar
import okhttp3.mockwebserver.Dispatcher
@@ -52,10 +50,6 @@ class DavDocumentsProviderTest {
@Inject
lateinit var db: AppDatabase
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@Inject
lateinit var testDispatcher: TestDispatcher
@@ -92,7 +86,7 @@ class DavDocumentsProviderTest {
@Test
fun testDoQueryChildren_insert() = runTest(mainDispatcher) {
fun testDoQueryChildren_insert() = runTest {
// Create parent and root in database
val id = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(id)
@@ -113,7 +107,7 @@ class DavDocumentsProviderTest {
}
@Test
fun testDoQueryChildren_update() = runTest(mainDispatcher) {
fun testDoQueryChildren_update() = runTest {
// Create parent and root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)
@@ -149,7 +143,7 @@ class DavDocumentsProviderTest {
}
@Test
fun testDoQueryChildren_delete() = runTest(mainDispatcher) {
fun testDoQueryChildren_delete() = runTest {
// Create parent and root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)
@@ -173,7 +167,7 @@ class DavDocumentsProviderTest {
}
@Test
fun testDoQueryChildren_updateTwoDirectoriesSimultaneously() = runTest(mainDispatcher) {
fun testDoQueryChildren_updateTwoDirectoriesSimultaneously() = runTest {
// Create root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)

View File

@@ -4,10 +4,8 @@
package at.bitfire.davdroid.webdav
import at.bitfire.davdroid.di.MainDispatcher
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
@@ -24,10 +22,6 @@ class WebDavMountRepositoryTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@Inject
lateinit var repository: WebDavMountRepository
@@ -40,13 +34,13 @@ class WebDavMountRepositoryTest {
val url = web.url("/")
@Test
fun testHasWebDav_NoDavHeader() = runTest(mainDispatcher) {
fun testHasWebDav_NoDavHeader() = runTest {
web.enqueue(MockResponse().setResponseCode(200))
assertFalse(repository.hasWebDav(url, null))
}
@Test
fun testHasWebDav_DavClass1() = runTest(mainDispatcher) {
fun testHasWebDav_DavClass1() = runTest {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV: 1"))
@@ -54,7 +48,7 @@ class WebDavMountRepositoryTest {
}
@Test
fun testHasWebDav_DavClass2() = runTest(mainDispatcher) {
fun testHasWebDav_DavClass2() = runTest {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV: 1, 2"))
@@ -62,7 +56,7 @@ class WebDavMountRepositoryTest {
}
@Test
fun testHasWebDav_DavClass3() = runTest(mainDispatcher) {
fun testHasWebDav_DavClass3() = runTest {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV: 1, 3"))

View File

@@ -2,7 +2,7 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.log
package at.bitfire.davdroid.di
import dagger.Module
import dagger.Provides