All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squareup.workflow.testing.WorkerTester.kt Maven / Gradle / Ivy

There is a newer version: 1.0.0-alpha.1
Show newest version
/*
 * Copyright 2019 Square Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.squareup.workflow.testing

import com.squareup.workflow.Worker
import com.squareup.workflow.testing.WorkflowTester.Companion.DEFAULT_TIMEOUT_MS
import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.produceIn
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.yield

interface WorkerTester {

  /**
   * Suspends until the worker emits its next value, then returns it.
   */
  suspend fun nextOutput(): T

  /**
   * Throws an [AssertionError] if an output has been emitted since the last call to [nextOutput].
   */
  fun assertNoOutput()

  /**
   * Suspends until the worker emits an output or finishes.
   *
   * Throws an [AssertionError] if an output was emitted.
   */
  suspend fun assertFinished()

  /**
   * Throws an [AssertionError] immediately if the worker is finished.
   */
  fun assertNotFinished()

  /**
   * Suspends until the worker throws an exception, then returns it.
   */
  suspend fun getException(): Throwable

  /**
   * Cancels the worker and suspends until it's finished cancelling (joined).
   */
  suspend fun cancelWorker()
}

/**
 * Test a [Worker] by defining assertions on its output within [block].
 */
fun  Worker.test(
  timeoutMs: Long = DEFAULT_TIMEOUT_MS,
  block: suspend WorkerTester.() -> Unit
) {
  runBlocking {
    supervisorScope {
      val channel: ReceiveChannel = run().produceIn(this + Unconfined)

      val tester = object : WorkerTester {
        override suspend fun nextOutput(): T = channel.receive()

        override fun assertNoOutput() {
          if (!channel.isEmpty) {
            throw AssertionError("Expected no output to have been emitted.")
          }
        }

        override suspend fun assertFinished() {
          try {
            val output = channel.receive()
            throw AssertionError("Expected Worker to finish, but emitted output: $output")
          } catch (e: ClosedReceiveChannelException) {
            // Expected.
          }
        }

        override fun assertNotFinished() {
          if (channel.isClosedForReceive) {
            throw AssertionError("Expected Worker to not be finished.")
          }
        }

        override suspend fun getException(): Throwable = try {
          val output = channel.receive()
          throw AssertionError("Expected Worker to throw an exception, but emitted output: $output")
        } catch (e: Throwable) {
          e
        }

        override suspend fun cancelWorker() {
          channel.cancel()
        }
      }

      // Yield to let the produce coroutine start, since we can't specify UNDISPATCHED.
      yield()

      withTimeout(timeoutMs) {
        block(tester)
      }

      coroutineContext.cancelChildren()
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy