23 votes

Coroutines de test unitaire runBlockingTest : Ce travail n'est pas encore terminé

Veuillez trouver ci-dessous une fonction utilisant une coroutine pour remplacer le callback :

override suspend fun signUp(authentication: Authentication): AuthenticationError {
    return suspendCancellableCoroutine {
        auth.createUserWithEmailAndPassword(authentication.email, authentication.password)
            .addOnCompleteListener(activityLifeCycleService.getActivity()) { task ->
                if (task.isSuccessful) {
                    it.resume(AuthenticationError.SignUpSuccess)
                } else {
                    Log.w(this.javaClass.name, "createUserWithEmail:failure", task.exception)
                    it.resume(AuthenticationError.SignUpFail)
                }
            }
    }
}

Je voudrais maintenant tester cette fonction à l'unité. J'utilise Mockk :

  @Test
  fun `signup() must be delegated to createUserWithEmailAndPassword()`() = runBlockingTest {

      val listener = slot<OnCompleteListener<AuthResult>>()
      val authentication = mockk<Authentication> {
        every { email } returns "email"
        every { password } returns "pswd"
      }
      val task = mockk<Task<AuthResult>> {
        every { isSuccessful } returns true
      }

      every { auth.createUserWithEmailAndPassword("email", "pswd") } returns
          mockk {
            every { addOnCompleteListener(activity, capture(listener)) } returns mockk()
          }

    service.signUp(authentication)

      listener.captured.onComplete(task)
    }

Malheureusement, ce test a échoué en raison de l'exception suivante : java.lang.IllegalStateException: This job has not completed yet

J'ai essayé de remplacer runBlockingTest avec runBlocking mais le test semble attendre dans une boucle infinie.

Quelqu'un peut-il m'aider avec cette UT ?

Merci d'avance

6voto

Comme on peut le voir dans ce poste :

Cette exception signifie généralement que certaines coroutines de vos tests ont été programmées en dehors de la portée du test (plus précisément le distributeur du test).

Au lieu de faire ça :

private val networkContext: CoroutineContext = TestCoroutineDispatcher()

private val sut = Foo(
  networkContext,
  someInteractor
)

fun `some test`() = runBlockingTest() {
  // given
  ...

  // when
  sut.foo()

  // then
  ...
}

Créer une portée de test en passant par le distributeur de test :

private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val networkContext: CoroutineContext = testDispatcher

private val sut = Foo(
  networkContext,
  someInteractor
)

Ensuite, dans le test effectué testScope.runBlockingTest

fun `some test`() = testScope.runBlockingTest {
  ...
}

Voir également l'article de Craig Russell "Test unitaire des fonctions de suspension des coroutines à l'aide de TestCoroutineDispatcher"

4voto

Heitor Colangelo Points 446

Il ne s'agit pas d'une solution officielle, utilisez-la donc à vos risques et périls.

C'est similaire à ce que @azizbekian a posté, mais au lieu d'appeler runBlocking vous appelez launch . Comme il s'agit d'utiliser TestCoroutineDispatcher les tâches dont l'exécution est prévue sans délai sont immédiatement exécutées. Cela peut ne pas convenir si vous avez plusieurs tâches qui s'exécutent de manière asynchrone.

Il se peut qu'elle ne convienne pas à tous les cas, mais j'espère qu'elle sera utile pour les cas simples.

Vous pouvez également suivre l'évolution de cette question ici :

Si vous savez comment résoudre ce problème en utilisant l'outil déjà existant runBlockingTest y runBlocking s'il vous plaît, soyez gentil et partagez avec la communauté.

class MyTest {
    private val dispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(dispatcher)

    @Test
    fun myTest {
       val apiService = mockk<ApiService>()
       val repository = MyRepository(apiService)

       testScope.launch {
            repository.someSuspendedFunction()
       }

       verify { apiService.expectedFunctionToBeCalled() }
    }
}

0voto

Sira Lam Points 1428

D'après ce que j'ai compris, cette exception se produit lorsque vous utilisez un dispatcher différent dans votre code à l'intérieur de l'application runBlockingTest { } avec celui qui a commencé runBlockingTest { } .

Donc pour éviter cela, vous devez d'abord vous assurer que vous injecter les dispatchers dans votre code, au lieu de le coder en dur dans toute votre application. Si vous ne l'avez pas fait, il n'y a pas d'endroit où commencer car vous ne pouvez pas assigner un distributeur de test à vos codes de test.

Ensuite, dans votre BaseUnitTest vous devriez avoir quelque chose comme ça :

@get:Rule
val coroutineRule = CoroutineTestRule()

@ExperimentalCoroutinesApi
class CoroutineTestRule(
    val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

L'étape suivante dépend vraiment de la façon dont vous faites l'injection de la Dépendance. L'essentiel est de s'assurer que vos codes de test utilisent le protocole coroutineRule.testDispatcher après l'injection.

Enfin, appelez runBlockingTest { } de ce testDispatcher :

@Test
fun `This should pass`() = coroutineRule.testDispatcher.runBlockingTest {
    //Your test code where dispatcher is injected
}

0voto

Vous devez terminer la valeur de l'assertion en appelant finish)`.

runBlockingTest {

flow.test(this).assertValues(someValue).finish()`

}

-1voto

Comme je l'ai mentionné aquí sur la fixation runBlockingTest peut-être que ça peut vous aider aussi.

Ajoutez cette dépendance si vous ne l'avez pas.

testImplementation "androidx.arch.core:core-testing:$versions.testCoreTesting" (2.1.0)

Ensuite, dans votre classe de test, déclarez la règle InstantTaskExecutorRule :

@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X