Test-Driven Development
SkillDev toolsTest-first workflow for features, bug fixes, refactors, and project-specific test patterns.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Test-Driven Development skill
What this skill tells your AI
The instructions your AI receives, as published by po4yka/ripdpi in .agents/skills/tdd/SKILL.md and read by ahel’s review.
This skill is RIGID. Follow every step exactly. Do not skip RED. Do not write implementation before a failing test exists.
Full test stack documentation: docs/testing.md. Do not duplicate it here -- read it when you need runner details, CI lanes, or fixture locations.
Workflow
Repeat this cycle for every behavior change:
- RED -- Write exactly one failing test. Run it. Confirm it fails for the expected reason.
- GREEN -- Write the minimum implementation to make the test pass. Run the test again.
- REFACTOR -- Clean up test and implementation. Run tests to confirm nothing broke.
- LINT -- Run
./gradlew staticAnalysisbefore considering the cycle complete.
Never batch multiple behaviors into one cycle. One test, one behavior, one cycle.
Running Tests
Kotlin/JVM (fast iteration)
# Single test method
./gradlew :core:engine:testDebugUnitTest --tests "ClassName.method name"
# Single test class
./gradlew :core:engine:testDebugUnitTest --tests "ClassName"
# Single module
./gradlew :core:engine:testDebugUnitTest
Replace :core:engine with the target module (:core:service, :core:data, :core:diagnostics).
Rust
# Single test
cargo nextest run --locked -p crate_name test_name
# Single crate
cargo nextest run --locked -p crate_name
# Full workspace
cargo nextest run --locked --workspace
Golden contracts
Golden tests are read-only by default. If your change intentionally alters a contract:
# Bless all golden fixtures
bash scripts/tests/bless-telemetry-goldens.sh
# Manual single-suite bless
RIPDPI_BLESS_GOLDENS=1 ./gradlew :core:engine:testDebugUnitTest
RIPDPI_BLESS_GOLDENS=1 cargo test --locked -p crate_name
Always review blessed diffs before committing. Golden changes require explanation in the commit message.
Test Double Conventions
Fake* classes (no mocking frameworks)
All test doubles are hand-written Fakes in core/engine/src/test/kotlin/com/poyka/ripdpi/core/TestDoubles.kt. The project does not use MockK, Mockito, or any mocking library.
Pattern:
- Name:
Fake+ interface name (e.g.,FakeRipDpiProxyRuntime) - Track call counts and last arguments as public properties
- Return configurable values set before the test runs
FaultQueue for fault injection
Fault injection uses FaultQueue<T> from core/engine-api/src/main/kotlin/com/poyka/ripdpi/core/testing/FaultModel.kt.
Key types:
FaultQueue<T>-- ordered queue of faults matched by target enumFaultSpec<T>-- target + outcome + scope + optional message/payloadFaultScope.ONE_SHOT-- fires once then is consumedFaultScope.PERSISTENT-- fires on every matching call until clearedFaultOutcome--EXCEPTION,TIMEOUT,DROP,RESET,MALFORMED_PAYLOAD,BLANK_PAYLOAD,PANIC
Usage in tests:
val bindings = FakeRipDpiProxyBindings()
bindings.faults.enqueue(
FaultSpec(
target = ProxyBindingFaultTarget.START,
outcome = FaultOutcome.EXCEPTION,
message = "simulated native crash",
)
)
Test Organization by Layer
| What you test | Location | Runner |
|---|---|---|
| Kotlin business logic | core/*/src/test/ | ./gradlew :core:*:testDebugUnitTest |
| Rust native logic | native/rust/crates/*/tests/ | cargo nextest run --locked -p crate |
| JNI integration | app/src/androidTest/.../integration/ | connectedGithubFullDebugAndroidTest |
| Network E2E | app/src/androidTest/.../e2e/ | connectedGithubFullDebugAndroidTest |
| Rust network E2E | native/rust/crates/*/tests/ | bash scripts/ci/run-rust-network-e2e.sh |
Subagent Strategy
For non-trivial features, use context isolation:
- Subagent writes the test -- Launch Claude Code's built-in Explore or general-purpose subagent to write the failing test. This keeps test design independent of implementation bias.
- Main context implements -- Read the test the subagent wrote, then implement the minimum code to pass it.
- Main context refactors -- Clean up both test and implementation in the same context.
This prevents the "write test and implementation together" anti-pattern.
Rules
- Never skip RED. Every test must fail before you write implementation.
- One test at a time. Do not write the next test until the current cycle is complete.
- Run staticAnalysis before commit.
./gradlew staticAnalysiscovers detekt, ktlint, and Android lint. It applies to test code too. - Test and implementation in one commit. Never commit a test without its implementation or vice versa.
- Fake doubles only.* No mocking frameworks. Add new Fakes to
TestDoubles.kt. - FaultQueue for error paths. Use
FaultSpec+FaultQueue, not ad-hoc exception throwing. - Golden contracts are read-only by default. Bless intentionally, review the diff, explain in the commit message.
- Use backtick names in Kotlin tests.
@Test funproxy start propagates native exception(). - Use snake_case names in Rust tests.
fn proxy_start_propagates_native_exception(). - Prefer unit tests. Only escalate to integration/E2E when the behavior requires real Android or network components.
Common Mistakes
| Mistake | Fix |
|---|---|
| Using MockK or Mockito | Write a Fake* class in TestDoubles.kt |
| Skipping the RED step | Run the test first. If it passes, your test is wrong. |
| Multiple assertions per cycle | Split into separate test methods, one behavior each |
runBlocking in coroutine tests | Use runTest from kotlinx-coroutines-test |
| Blessing goldens without review | Run git diff on fixture files before committing |
| Testing private internals | Test through the public API of the class under test |
| Putting helpers in test classes | Add shared helpers to TestDoubles.kt |
FaultScope.PERSISTENT when ONE_SHOT suffices | Default to ONE_SHOT; use PERSISTENT only for repeated-call scenarios |
For deeper coverage of anti-patterns, see references/testing-anti-patterns.md.
Signals
- GitHub stars
- 69
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
tdd-po4yka- Source
- github.com/po4yka/ripdpi