使用单元测试支持清洁Kotlin中的Coroutines使用

时间:2017-10-03 14:16:11

标签: android unit-testing asynchronous kotlin kotlinx.coroutines

我们一段时间以来一直在与Kotlin合作,而我们目前关注的一件事就是使用Coroutines来处理我们想要运行异步的操作。

虽然示例用法很明确且有效,但我在构建中以一种干净的方式将这一问题集成在一起。在研究以域为中心的课程的方法实现时,我们的想法是它易于阅读,而且噪音更少"尽可能从异步功能。我知道在没有实际使用它的情况下我不能同步。所以写下这样的东西就是我喜欢的:

val data = someService.getData().await()
// work with data

但这是我想要阻止的:

launch(UI) {
  val data 
  val job = async(CommonPool) {
    data = someService.getData()
  }

  job.await()
  // work with data 
}

对于这些以域名为中心的课程,我希望与实际的单元测试配对,但我无法真正实现这一点。让我们看一个例子:

// Some dependency doing heavy work
class ApiClient {
    suspend fun doExpensiveOperation(): String {
        delay(1000)

        return "Expensive Result Set"
    }
}

// Presenter Class
class Presenter(private val apiClient: ApiClient,
                private val view: TextView) {

    private lateinit var data: String

    fun start() {
        log("Starting Presenter")
        runBlocking {
            log("Fetching necessary data")
            data = apiClient.doExpensiveOperation()
            log("Received necessary data")
        }

        workWithData()

        log("Started Presenter")
    }

    fun workWithData() {
        log(data)
    }

    private fun log(text: String) {
        view.append(text+"\n")
    }
}

// In an Activity
val presenter = Presenter(ApiClient(), someTextView)
presenter.start()

有效(截图:https://imgur.com/a/xG9Xw)。现在让我们来看看测试。

class PresenterTest {
    // ... Declared fields

    @Before
    fun setUp() {
        // Init mocks (apiClient, textView)
        MockitoAnnotations.initMocks(this)

        // Set mock responses
        runBlocking {
            given(apiClient.doExpensiveOperation()).willReturn("Some Value")
        }

        presenter = Presenter(apiClient, textView)
    }

    @Test
    @Throws(Exception::class)
    fun testThat_whenPresenterStarts_expectedResultShows() {
        // When
        presenter.start()

        // Then
        Mockito.verify(textView).text = "Some Value\n"
    }
}

现在这个测试不太理想,但无论如何,它甚至都没有达到可以验证事情按预期工作的程度,因为初始var数据没有被初始化。现在最终我们的领域课程的美学和可读性就是我想走多远,我有一些实用的工作实例,我很满意。但让我的测试工作似乎具有挑战性。

现在网上有一些关于这种东西的不同文章,但对我来说没有任何结果。这个(https://medium.com/@tonyowen/android-kotlin-coroutines-unit-test-16e984ba35b4)似乎很有趣,但我不喜欢调用类为演示者启动上下文的想法,因为这反过来又有一个依赖,可以做一些异步工作。虽然作为一个抽象的想法,我喜欢" Hey主持人的想法,无论你做什么,都要在UI上下文中向我报告,它更像是一个让事情有效的解决方案,导致共同的关注不同对象的异步功能。

无论如何,我的问题是: 从简短的例子开始,有没有人对如何在更大的架构中集成协同程序以及工作单元测试有任何指示?我也非常愿意接受让我改变观察方式的论据,因为它在不同的层面上令人信服,而且#34;如果你想让事情发挥作用,你有牺牲。"。这个问题不仅仅是让这个例子有用,因为这只是一个孤立的例子,而我正在寻找一个大项目中真正可靠的集成。

期待您的投入。提前谢谢。

1 个答案:

答案 0 :(得分:1)

我建议使用某种AsyncRunner接口的方法,并且有两个AsyncRunner接口的实现。一个是使用launch(UI)的Android实现,另一个是使用runBlocking的一些阻止实现。

将正确类型的AsyncRunner传递到app中运行的代码中,并且在单元测试中运行的代码应该通过依赖注入来完成。在您的代码中,您不能直接使用协程,而是使用注入的AsyncRunner来运行异步代码。

AsyncRunner的示例实现可能如下所示:

interface AsyncRunner {
    fun <T>runAsync(task: () -> T, completion: (T) -> Unit)
}

class AndroidCoroutineAsyncRunner: AsyncRunner {
    override fun <T>runAsync(task: () -> T, completion: (T) -> Unit) {
        launch(UI) {
            completion(async(CommonPool) { task() }.await())
        }
    }
}

class BlockingCoroutineAsyncRunner: AsyncRunner {
    override fun <T>runAsync(task: () -> T, completion: (T) -> Unit) {
        runBlocking {
            completion(async(CommonPool) { task() }.await())
        }
    }
}

其中task参数表示线程阻塞代码(例如从API获取数据),completion参数将从任务中获取数据并对其执行某些操作。