我想创建一个可以处理事件的长期服务。
它通过postEvent
接收事件,将其存储在存储库中(带有底层数据库),并在事件足够多时以api的形式发送它们。
我也想按需关闭它。 此外,我想测试这项服务。
这是我到目前为止提出的。目前,我正在努力对其进行单元测试。
通过fixture.postEvent()
将事件发送到服务后,数据库可能会过早关闭,或者测试本身陷入某种死锁状态(正在尝试各种上下文+作业配置)。
我在这里做什么错了?
class EventSenderService(
private val repository: EventRepository,
private val api: Api,
private val serializer: GsonSerializer,
private val requestBodyBuilder: EventRequestBodyBuilder,
) : EventSender, CoroutineScope {
private val eventBatchSize = 25
val job = Job()
private val channel = Channel<Unit>()
init {
job.start()
launch {
for (event in channel) {
val trackingEventCount = repository.getTrackingEventCount()
if (trackingEventCount < eventBatchSize) continue
readSendDelete()
}
}
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job
override fun postEvent(event: Event) {
launch(Dispatchers.IO) {
writeEventToDatabase(event)
}
}
override fun close() {
channel.close()
job.cancel()
}
private fun readSendDelete() {
try {
val events = repository.getTrackingEvents(eventBatchSize)
val request = requestBodyBuilder.buildFor(events).blockingGet()
api.postEvents(request).blockingGet()
repository.deleteTrackingEvents(events)
} catch (throwable: Throwable) {
Log.e(throwable)
}
}
private suspend fun writeEventToDatabase(event: Event) {
try {
val trackingEvent = TrackingEvent(eventData = serializer.toJson(event))
repository.insert(trackingEvent)
channel.send(Unit)
} catch (throwable: Throwable) {
throwable.printStackTrace()
Log.e(throwable)
}
}
}
测试
@RunWith(RobolectricTestRunner::class)
class EventSenderServiceTest : CoroutineScope {
@Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
private val api: Api = mock {
on { postEvents(any()) } doReturn Single.just(BaseResponse())
}
private val serializer: GsonSerializer = mock {
on { toJson<Any>(any()) } doReturn "event_data"
}
private val bodyBuilder: EventRequestBodyBuilder = mock {
on { buildFor(any()) } doReturn Single.just(TypedJsonString.buildRequestBody("[ { event } ]"))
}
val event = Event(EventName.OPEN_APP)
private val database by lazy {
Room.inMemoryDatabaseBuilder(
RuntimeEnvironment.systemContext,
Database::class.java
).allowMainThreadQueries().build()
}
private val repository by lazy { database.getRepo() }
val fixture by lazy {
EventSenderService(
repository = repository,
api = api,
serializer = serializer,
requestBodyBuilder = bodyBuilder,
)
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + fixture.job
@Test
fun eventBundling_success() = runBlocking {
(1..40).map { Event(EventName.OPEN_APP) }.forEach { fixture.postEvent(it) }
fixture.job.children.forEach { it.join() }
verify(api).postEvents(any())
assertEquals(15, eventDao.getTrackingEventCount())
}
}
按照@Marko Topolnik的建议更新代码后-添加fixture.job.children.forEach { it.join() }
测试永远不会完成。
答案 0 :(得分:1)
您做错的一件事与此有关:
override fun postEvent(event: Event) {
launch(Dispatchers.IO) {
writeEventToDatabase(event)
}
}
postEvent
启动即发即弃的异步作业,该作业最终会将事件写入数据库。您的测试会快速连续创建40个此类作业,并且在排队时会声明预期状态。不过,我不知道为什么您要在发布40个事件后断言15个事件。
要解决此问题,您应该使用已有的行:
fixture.job.join()
但将其更改为
fixture.job.children.forEach { it.join() }
将其放低,在创建事件的循环之后。
我没有考虑到您在init
块中启动的长期消费者工作。这使我上面提出的加入主工作的所有孩子的建议无效。
相反,您将不得不进行更多更改。使postEvent
返回它启动的作业,并在测试中收集所有这些作业并加入。这样更具选择性,并且避免参加长期工作。
作为一个单独的问题,您的批处理方法并不理想,因为在执行任何操作之前,它将始终等待完整的批处理。每当出现无事件的休整期时,事件将无限期地处于不完整的批次中。
最好的方法是自然批处理,在这种情况下,您将不断消耗输入队列。当有大量传入事件发生时,批次自然会增长,当它们滴入时,仍将立即提供。您可以看到基本概念here。