【问题标题】:How to run a Test synchronously which uses a shareIn Flow?如何同步运行使用 shareIn Flow 的测试?
【发布时间】:2022-11-11 17:38:35
【问题描述】:

设想

我正在测试myTest()StateFlow 的发射。该测试调用了一个函数viewModel.initiateFlow(),它在 3 个存储库中启动了一个流链并增加了一个按钮文本。

目标

我想让测试按顺序运行,以便它在testDispatcher 上同步运行整个流程,并等待它在 ViewModel 中收集,然后再继续 runTest。

问题

目前assertTextEquals 失败,因为流是异步运行的,没有足够长的时间在MyViewModel() 中收集和更新。

下面是一个图表和相应的代码,可以最好地解释正在发生的事情。 FakeRepository2() 正在使用 shareIn(),我认为这是导致问题的原因。 onEach{} 中的长时间运行代码(模拟延迟)在单独的默认调度程序上异步运行。我为此使用 GlobalScope()。我曾尝试传入一个 testScope,但在这种情况下,onEach{} 从未运行过。我怎样才能解决这个问题?

代码


@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
class MyTest {
    private val testScheduler = TestCoroutineScheduler()
    private val testDispatcher = StandardTestDispatcher(testScheduler)
    private val testScope = TestScope(testDispatcher)

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @get:Rule
    val composeRule = createComposeRule()

    private lateinit var repository1: FakeRepository1
    private lateinit var repository2: FakeRepository2
    private lateinit var repository3: FakeRepository3
    private lateinit var viewModel: MyViewModel

    @Before
    fun setUp() {

        repository1 = FakeRepository1()
        repository2 = FakeRepository2(repository1, testScope)
        repository3 = FakeRepository3(repository2)

        viewModel = MyViewModel(repository1, repository3)

        composeRule.setContent {
            AppCompatTheme {
                MyComposableForTest(viewModel = viewModel)
            }
        }
    }

    @Test
    fun myTest() = testScope.runTest {

        composeRule.onRoot(useUnmergedTree = true).printToLog("MY_TAG")

        composeRule.onNodeWithContentDescription("Button One").assertIsDisplayed()

        //start the flow
        viewModel.initiateFlow()

        //assertion fails because Repository2 onEach{} block is taking long time to run and being ran asynchronously outside of test-Dispacther.
        composeRule.onNodeWithContentDescription("Button One")
            .assertTextEquals("1")

        //remaining test code

    }

}



class MyViewModel @Inject constructor(
    private val repository1: IRepository1,
    private val repository3: IRepository3
) : ViewModel() {

    private var _myInt = MutableStateFlow(0)
    val myInt = _myInt.asStateFlow()

    init {
        viewModelScope.launch{
            repository3.getFlow.collect{
                _myInt.value = it
                it.toString()
            }
        }
    }


    fun initiateFlow() = viewModelScope.launch {
        repository1.emitSharedFlow(_myInt.value)
    }

}


class FakeRepository1() : IRepository1 {

    override val _sharedFlow = MutableSharedFlow<Int>()
    override val sharedFlow = _sharedFlow.asSharedFlow()

    override suspend fun emitSharedFlow(myInt: Int) {
        _sharedFlow.emit(myInt)
    }
}


class FakeRepository2 @OptIn(ExperimentalCoroutinesApi::class) constructor(
    private val repository1: FakeRepository1,
    private val scope: TestScope
): IRepository2 {
    @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
    override val getFlow = repository1.sharedFlow
        .onEach {
            //doSomething
            delay(1000)
            delay(2000)
            delay(3000)
        }
        .shareIn(
            GlobalScope,
            SharingStarted.WhileSubscribed()
        )
}


class FakeRepository3(
    private val repository2: FakeRepository2
): IRepository3 {

    override val getFlow = repository2.getFlow
        .map {
            var increment = it
            increment += 1
            increment
        }
}


@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel =
            ViewModelProvider(
                this, ViewModelProvider.AndroidViewModelFactory.getInstance(this.application)
            )[MyViewModel::class.java]
        setContent {
            MyComposableForTest(viewModel)
        }

    }
}

@Composable
fun MyComposableForTest(viewModel: MyViewModel) {

    val buttonOne by viewModel.myInt.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .semantics {
                contentDescription = "Button Column"
            },
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {

        Button(
            modifier = Modifier
                .semantics {
                    contentDescription = "Button One"
                },
            onClick = { viewModel.initiateFlow() }
        ) {
            Text(
                text = buttonOne.toString(),
                modifier = Modifier
                    .semantics {
                        contentDescription = "Button One Text"
                    }
            )
        }
    }

}

【问题讨论】:

    标签: android android-espresso android-testing kotlin-flow kotlin-stateflow


    【解决方案1】:

    为了确保FakeRepository2.onEach{} 同步运行,我使用UnconfinedTestDispatcher() 按照Testing Kotlin Flows with shareIn() 立即在当前(测试)线程上启动协程。

    【讨论】:

      猜你喜欢
      • 2020-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-09
      • 2015-01-31
      • 2020-05-21
      • 1970-01-01
      • 2016-08-23
      相关资源
      最近更新 更多