Use when writing or structuring unit tests in stripe-android — covers runScenario pattern, fakes, Turbine Flow testing, and Truth assertions
npx skills add https://github.com/stripe/stripe-android --skill write-unit-tests
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 isolatedToolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when implementing any feature or bugfix, before writing implementation code
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
Cloud laboratory platform for automated protein testing and validation. Use when designing proteins and needing experimental validation including binding assays, expression testing, thermostability measurements, enzyme activity assays, or protein sequence optimization. Also use for submitting experiments via API, tracking experiment status, downloading results, optimizing protein sequences for better expression using computational tools (NetSolP, SoluProt, SolubleMPNN, ESM), or managing protein design workflows with wet-lab validation.
This skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.
Take stripe/write-unit-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.