一键导入
write-unit-tests
Use when writing or structuring unit tests in stripe-android — covers runScenario pattern, fakes, Turbine Flow testing, and Truth assertions
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing or structuring unit tests in stripe-android — covers runScenario pattern, fakes, Turbine Flow testing, and Truth assertions
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns
Use when creating a fake test implementation in stripe-android — covers FakeClassName pattern, Turbine call tracking, ViewActionRecorder, and ensureAllEventsConsumed validation
Use when writing NetworkRule integration tests in stripe-android — covers testBodyFromFile, inline JSON modification, request matchers, and fixture patterns
Use when writing or running Paparazzi screenshot tests in stripe-android — covers PaparazziRule setup, recording/verifying commands, and test structure
| name | write-unit-tests |
| description | Use when writing or structuring unit tests in stripe-android — covers runScenario pattern, fakes, Turbine Flow testing, and Truth assertions |
This skill describes how to structure tests in the Stripe Android SDK using fakes, scenarios, and proper verification patterns.
create-fake skill)runScenario functions to organize test setupensureAllEventsConsumed() on fakes after test blockassertThat(actual).isEqualTo(expected) from Google Truth@Test should cover a single scenario or configuration.test { } syntaxEvery test should follow this pattern:
@Test
fun `test description`() = runScenario(
// Test-specific parameters
config = testConfig
) {
// 1. Configure: Set up fake behaviors (optional)
fakeService.result = expectedResult
// 2. Execute: Call the code under test
val result = systemUnderTest.doSomething()
// 3. Verify: Assert results and check fake calls
assertThat(result.status).isEqualTo(expectedStatus)
assertThat(result.id).isEqualTo(expectedId)
assertThat(fakeService.calls.awaitItem()).isEqualTo(expectedCall)
}
// 4. Validation: ensureAllEventsConsumed called automatically by runScenario
Create a runScenario function and a Scenario class at the bottom of your test file:
class MyFeatureTest {
@Test
fun `test case`() = runScenario {
// Test code using scenario fields
assertThat(systemUnderTest.getValue()).isEqualTo(expectedValue)
}
private fun runScenario(
config: Config = defaultConfig,
block: suspend Scenario.() -> Unit,
) = runTest {
// Setup fakes
val fakeRepository = FakeRepository()
val fakeAnalytics = FakeAnalytics()
// Create system under test
val systemUnderTest = MyFeature(
repository = fakeRepository,
analytics = fakeAnalytics,
config = config,
)
// Run test block with scenario context
Scenario(
systemUnderTest = systemUnderTest,
fakeRepository = fakeRepository,
fakeAnalytics = fakeAnalytics,
).apply { block() }
// Validate all fakes
fakeRepository.ensureAllEventsConsumed()
fakeAnalytics.ensureAllEventsConsumed()
}
private data class Scenario(
val systemUnderTest: MyFeature,
val fakeRepository: FakeRepository,
val fakeAnalytics: FakeAnalytics,
)
}
Key Features:
runScenario replaces runTest as the test entry pointensureAllEventsConsumed() called automatically after test block@Test
fun `fetching data returns success when repository succeeds`() = runScenario {
// Configure fake behavior
fakeRepository.dataResult = Result.success(testData)
// Execute
val result = systemUnderTest.fetchData()
// Verify
assertThat(result.isSuccess).isTrue()
assertThat(fakeRepository.fetchCalls.awaitItem()).isEqualTo(FetchCall(userId = "123"))
}
// ensureAllEventsConsumed called automatically
When the code under test returns an object, prefer asserting on the specific fields you care about instead of asserting that the whole object is equal to an expected object:
assertThat(result.status).isEqualTo(Status.Complete)
assertThat(result.paymentMethodId).isEqualTo("pm_123")
Use whole-object equality only when exact object equality is the behavior under test.
Field-level assertions usually produce better failure messages because the failing property is obvious.
Keep each unit test focused on a single case. If you need to verify several related configurations, write one test per configuration instead of combining them into one test.
@Test
fun `analytics event for saved card`() = runScenario(
paymentSelection = PaymentSelection.Saved(paymentMethod),
) {
assertThat(fakeAnalytics.calls.awaitItem().event).isEqualTo("saved_card")
}
@Test
fun `analytics event for new card`() = runScenario(
paymentSelection = PaymentSelection.New.Card(cardParams),
) {
assertThat(fakeAnalytics.calls.awaitItem().event).isEqualTo("new_card")
}
This keeps failures isolated. When multiple scenarios are combined into one test, execution stops at the first failing assertion and hides the rest of the failures from that run.
Use Turbine's .test { } to assert Flow emissions:
@Test
fun `state updates when data changes`() = runScenario {
systemUnderTest.state.test {
// Initial state
assertThat(awaitItem()).isEqualTo(State.Loading)
// Trigger change
fakeRepository.emit(newData)
assertThat(awaitItem()).isEqualTo(State.Loaded(newData))
// No more events expected
ensureAllEventsConsumed()
}
}
Common Turbine operations:
awaitItem() — wait for next emissionexpectNoEvents() — assert no emissions occurredensureAllEventsConsumed() — verify no unconsumed events remainskipItems(n) — skip past emissions you don't care about| What | Pattern |
|---|---|
| Test entry point | fun \test name`() = runScenario { }` |
| Assertions | assertThat(actual).isEqualTo(expected) |
| Flow testing | flow.test { assertThat(awaitItem()).isEqualTo(x) } |
| Fake call tracking | assertThat(fake.calls.awaitItem()).isEqualTo(call) |
| Fake validation | ensureAllEventsConsumed() — automatic in runScenario |
| Test granularity | One scenario or configuration per @Test |
| Compose UI tests | Invoke compose-tests skill |
| Creating fakes | Invoke create-fake skill |
| NetworkRule integration tests | Invoke network-tests skill |
When testing coroutines that involve real network I/O (NetworkRule/OkHttp), use runTest + testScheduler.advanceUntilIdle() + async/await().
When you need to verify a StateFlow's transitions during an async operation, use Turbine's .test { } to assert the full sequence:
@Test
fun `loading state transitions during mutation`() = runTest {
val holdResponse = CountDownLatch(1)
networkRule.enqueue(host("api.stripe.com"), method("POST"), path("/v1/...")) { response ->
holdResponse.await(10, TimeUnit.SECONDS)
response.setBody("{}")
}
systemUnderTest.isLoading.test {
assertThat(awaitItem()).isFalse()
val job = async { systemUnderTest.mutate() }
testScheduler.advanceUntilIdle()
assertThat(awaitItem()).isTrue()
holdResponse.countDown()
job.await()
assertThat(awaitItem()).isFalse()
}
}
When you need to verify that concurrent operations are serialized by a mutex, use CountDownLatch to observe request ordering:
@Test
fun `concurrent operations are serialized by mutex`() = runTest {
val holdFirstResponse = CountDownLatch(1)
val secondRequestArrived = CountDownLatch(1)
networkRule.enqueue(host("api.stripe.com"), method("POST"), path("/v1/...")) { response ->
holdFirstResponse.await(10, TimeUnit.SECONDS)
response.setBody("{}")
}
networkRule.enqueue(host("api.stripe.com"), method("POST"), path("/v1/...")) { response ->
secondRequestArrived.countDown()
response.setBody("{}")
}
val jobB = async { systemUnderTest.operationB() }
testScheduler.advanceUntilIdle()
val jobA = async { systemUnderTest.operationA() }
testScheduler.advanceUntilIdle()
assertThat(secondRequestArrived.count).isEqualTo(1) // A hasn't fired
holdFirstResponse.countDown()
jobB.await()
jobA.await()
assertThat(secondRequestArrived.count).isEqualTo(0) // A did fire
}
async + await() over launch + join() — await() propagates exceptionstestScheduler.advanceUntilIdle() over Thread.sleep — deterministic, advances to suspension pointawait() (not advanceUntilIdle() alone) — OkHttp delivers continuations asynchronouslyCountDownLatch.await() inside mock handlers — prevents hangs.test { } for StateFlow emission sequences; CountDownLatch for request orderingFakeClassName implementationsensureAllEventsConsumed() — runScenario handles this, but if using runTest directly, call it manuallyThread.sleep in tests — use testScheduler.advanceUntilIdle() insteadHashMap.clear() works@Tests so failures stay isolated