2

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

虽然示例用法很清楚并且有效,但我在我们的架构中以干净的方式集成它时遇到了一些问题。在查看以领域为中心的类的方法实现时,想法是它易于阅读,并且异步功能的“噪音”尽可能少。我知道我不能有异步,没有实际使用它。所以写这样的东西就是我想要的:

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"
    }
}

现在这个测试不太理想,但无论如何,它甚至无法验证事情是否按预期工作,因为没有初始化 lateinit var 数据。现在归根结底,我们的领域类的美观和可读性就是我想走多远,我有一些我很满意的实际工作示例。但是让我的测试工作似乎具有挑战性。

现在网上有一些关于这类东西的不同文章,但对我来说没有任何效果。这(https://medium.com/@tonyowen/android-kotlin-coroutines-unit-test-16e984ba35b4)看起来很有趣,但我不喜欢调用类为演示者启动上下文的想法,因为在turn 具有执行一些异步工作的依赖项。虽然作为一个抽象的想法,我喜欢“嘿,演示者,无论你做什么,就 UI 上下文向我报告”的想法,但它更像是一种使事情正常进行的修复,导致对不同对象的异步功能的共同关注.

无论如何,我的问题:远离简短的示例,是否有人对如何将协程集成到更大的架构中以及工作单元测试有任何指示?我也很愿意接受那些让我改变看待事物的方式的论点,因为它在不同的层面上具有说服力,而不是“如果你想让事情发挥作用,你就必须牺牲。”。这个问题不仅仅是让示例工作,因为这只是一个孤立的示例,而我正在寻找一个大项目中真正可靠的集成。

期待您的输入。提前致谢。

4

2 回答 2

1

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

将正确的类型传递给AsyncRunner在应用程序中运行的代码和在单元测试中运行的代码应该通过依赖注入来完成。然后,在您的代码中,您不会直接使用协程,而是使用注入的 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参数将从任务中获取数据并对其进行处理。

于 2018-01-07T15:49:36.613 回答
0

你应该放弃协程并改用 RxJava。在那里你会发现你所寻求的那种简洁和简单。当我问大多数开发人员为什么使用协程时,他们的回答总是一样的:“好吧,协程是新事物,我们应该使用谷歌的最新技术”。除了协程不是新的。它们于 1952 年左右首次引入(参见 Wikipedia 中的“协程”),作为进行异步软件开发的提议。很明显,计算机科学社区几年前拒绝协程,因为它不是异步编程的最佳方法。为什么 JetBrains 决定将一项旧的、被拒绝的技术引入 Kotlin,您必须询问 JetBrains。我不得不处理其他人已经编写了几年的代码中的协程,而且我总是发现协程不必要地复杂。当维护开发人员不得不处理由早已离开项目的开发人员编写的协程意大利面时,协程除了降低可维护性之外没有其他办法。

接下来我从这些开发人员那里听到的是,RxJava 是旧技术,而协程是新技术。如果他们做了他们的研究,他们永远不会做出如此离谱的错误陈述。恕我直言,RxJava 是整个计算机科学史上异步软件开发中最重要的新发展。

于 2021-12-14T12:09:38.640 回答