Complete guide to testing with KRelay, including test structure, utilities, and best practices.
- Test Structure
- Running Tests
- Test Utilities
- Writing Tests
- Demo Examples
- Best Practices
- Troubleshooting
KRelay tests are organized into three levels:
Test individual components in isolation:
- WeakRefTest.kt - Weak reference behavior
- QueuedActionTest.kt - Action wrapper with timestamps
- PriorityTest.kt - Priority enum and ordering
- MetricsTest.kt - Metrics recording and retrieval
Purpose: Verify each component works correctly on its own.
Test interactions between components:
- RegistryQueueIntegrationTest.kt - Register + Queue + Dispatch flows
- PriorityQueueIntegrationTest.kt - Priority + Queue interactions
- MetricsIntegrationTest.kt - Metrics tracking during operations
Purpose: Verify components work together correctly.
Test complete real-world scenarios:
- ScreenRotationScenarioTest.kt - Activity rotation lifecycle
- BackgroundForegroundScenarioTest.kt - App backgrounding scenarios
- ConcurrentOperationsScenarioTest.kt - Thread safety under load
Purpose: Verify the entire system works in production scenarios.
Advanced usage examples that double as tests:
- LoginFlowDemo.kt - Complete login flow with error handling
- DataSyncDemo.kt - Background sync with progress updates
- ErrorHandlingDemo.kt - Error handling patterns
- MultiFeatureCoordinationDemo.kt - Multi-feature workflows
Purpose: Show real-world usage patterns and serve as executable documentation.
./gradlew :krelay:test./gradlew :krelay:test --tests "dev.brewkits.krelay.unit.WeakRefTest"
./gradlew :krelay:test --tests "dev.brewkits.krelay.demo.LoginFlowDemo"# Unit tests only
./gradlew :krelay:test --tests "dev.brewkits.krelay.unit.*"
# Integration tests only
./gradlew :krelay:test --tests "dev.brewkits.krelay.integration.*"
# System tests only
./gradlew :krelay:test --tests "dev.brewkits.krelay.system.*"
# Demo examples only
./gradlew :krelay:test --tests "dev.brewkits.krelay.demo.*"./gradlew :krelay:test --info
./gradlew :krelay:test --debug./gradlew :krelay:test :krelay:jacocoTestReport
# Report will be in: krelay/build/reports/jacoco/test/html/index.htmlKRelay provides comprehensive test utilities in TestUtils.kt.
Pre-built mock implementations for quick testing:
// Basic test feature
val mock = MockTestFeature()
KRelay.register(mock)
KRelay.dispatch<TestFeature> { it.execute("test") }
assertEquals(listOf("test"), mock.executedValues)
// Toast feature
val toast = MockToast()
KRelay.register<TestToastFeature>(toast)
KRelay.dispatch<TestToastFeature> { it.show("Hello") }
assertTrue(toast.messages.contains("Hello"))
// Navigation feature
val nav = MockNavigation()
KRelay.register<TestNavigationFeature>(nav)
KRelay.dispatch<TestNavigationFeature> { it.navigate("home") }
assertEquals("home", nav.currentRoute)Simplified assertions for common checks:
// Registration assertions
assertRegistered<ToastFeature>()
assertNotRegistered<ToastFeature>()
// Queue assertions
assertQueueSize<ToastFeature>(3)
assertQueueEmpty<ToastFeature>()
assertQueueNotEmpty<ToastFeature>()
// Metrics assertions
assertMetrics<ToastFeature>(
dispatches = 5L,
queued = 2L,
replayed = 3L
)Simplify test setup/teardown:
// Automatic setup and cleanup
withKRelay(debugMode = true) {
// Your test code
KRelay.dispatch<TestFeature> { it.execute("test") }
}
// KRelay.reset() called automatically
// With metrics tracking
withMetrics {
// Your test code
}
// Metrics reset automaticallySimulate common Android scenarios:
// Activity lifecycle
simulateActivityLifecycle<ToastFeature>(
implementation = AndroidToast(),
onCreated = {
// Actions when created
},
onDestroyed = {
// Actions when destroyed
}
)
// Screen rotation
val toast1 = AndroidToast()
val toast2 = AndroidToast()
simulateRotation(
oldImplementation = toast1,
newImplementation = toast2,
actionsDuringRotation = {
KRelay.dispatch<ToastFeature> { it.show("Queued!") }
}
)
// Background/Foreground
simulateBackgrounding<ToastFeature>(
foregroundImplementation = toast1,
actionInBackground = {
KRelay.dispatch<ToastFeature> { it.show("Background") }
}
)
simulateForegrounding<ToastFeature>(toast2)Manipulate queues for testing:
// Fill queue with actions
fillQueue<ToastFeature>(count = 10)
assertQueueSize<ToastFeature>(10)
// Fill with priority actions
fillQueueWithPriority<ToastFeature>(
low = 2,
normal = 3,
high = 4,
critical = 1
)
assertQueueSize<ToastFeature>(10)Debug test issues:
// Print queue state for a feature
printQueueState<ToastFeature>()
// Output:
// === Queue State for ToastFeature ===
// Registered: true
// Pending: 3
// Metrics: {dispatches=5, queued=3, ...}
// Print all KRelay state
printAllState()End-to-end flow verification:
// Verify complete registration flow
verifyRegistrationFlow<ToastFeature>(
implementation = AndroidToast(),
expectedQueuedBefore = 2,
actionBeforeRegister = {
KRelay.dispatch<ToastFeature> { it.show("Test 1") }
KRelay.dispatch<ToastFeature> { it.show("Test 2") }
}
)
// Verify rotation preserves queued actions
verifyRotationFlow<ToastFeature>(
beforeRotation = toast1,
afterRotation = toast2,
actionDuringRotation = {
KRelay.dispatch<ToastFeature> { it.show("During rotation") }
},
verify = { implementation ->
assertTrue(implementation.messages.contains("During rotation"))
}
)package dev.brewkits.krelay
import kotlin.test.*
class MyFeatureTest {
@BeforeTest
fun setup() {
KRelay.reset()
KRelay.debugMode = true
}
@AfterTest
fun tearDown() {
KRelay.reset()
}
@Test
fun testMyFeature() {
// Given
val mock = MockTestFeature()
// When
KRelay.register(mock)
KRelay.dispatch<TestFeature> { it.execute("test") }
// Then
assertEquals(listOf("test"), mock.executedValues)
}
}@Test
fun testRegistration() {
// Before registration
assertFalse(KRelay.isRegistered<ToastFeature>())
// Register
val toast = AndroidToast()
KRelay.register(toast)
// After registration
assertTrue(KRelay.isRegistered<ToastFeature>())
// Unregister
KRelay.unregister<ToastFeature>()
// After unregister
assertFalse(KRelay.isRegistered<ToastFeature>())
}@Test
fun testQueueing() {
// Dispatch without registration (queues)
KRelay.dispatch<ToastFeature> { it.show("Message 1") }
KRelay.dispatch<ToastFeature> { it.show("Message 2") }
// Verify queued
assertEquals(2, KRelay.getPendingCount<ToastFeature>())
// Register (replays queue)
val toast = AndroidToast()
KRelay.register(toast)
// Verify queue cleared and actions replayed
assertEquals(0, KRelay.getPendingCount<ToastFeature>())
assertTrue(toast.messages.contains("Message 1"))
assertTrue(toast.messages.contains("Message 2"))
}@Test
fun testPriority() {
// Dispatch with different priorities
KRelay.dispatchWithPriority<ToastFeature>(ActionPriority.LOW) {
it.show("Low priority")
}
KRelay.dispatchWithPriority<ToastFeature>(ActionPriority.CRITICAL) {
it.show("Critical!")
}
// All queued
assertEquals(2, KRelay.getPendingCount<ToastFeature>())
// Register and verify
val toast = AndroidToast()
KRelay.register(toast)
// Both messages received (order depends on priority)
assertEquals(2, toast.messages.size)
}@Test
fun testRotation() {
// Before rotation
val toast1 = AndroidToast()
KRelay.register<ToastFeature>(toast1)
// Rotation starts (Activity destroyed)
KRelay.unregister<ToastFeature>()
// Action during rotation (queued)
KRelay.dispatch<ToastFeature> { it.show("During rotation") }
assertEquals(1, KRelay.getPendingCount<ToastFeature>())
// After rotation (new Activity)
val toast2 = AndroidToast()
KRelay.register<ToastFeature>(toast2)
// Verify replayed on new instance
assertEquals(0, KRelay.getPendingCount<ToastFeature>())
assertTrue(toast2.messages.contains("During rotation"))
}@Test
fun testMetrics() {
// Reset metrics
KRelayMetrics.reset()
// Perform operations
KRelay.dispatch<ToastFeature> { it.show("Test") }
KRelay.dispatch<ToastFeature> { it.show("Test 2") }
// Check metrics
val metrics = KRelay.getMetrics<ToastFeature>()
assertEquals(2L, metrics["dispatches"])
assertEquals(2L, metrics["queued"])
// Register and replay
KRelay.register<ToastFeature>(AndroidToast())
// Check updated metrics
val updatedMetrics = KRelay.getMetrics<ToastFeature>()
assertEquals(2L, updatedMetrics["replayed"])
}The demo examples showcase real-world usage patterns.
File: demo/LoginFlowDemo.kt
Shows complete login flow with:
- Loading states
- Network calls
- Success/error handling
- Analytics tracking
- Rotation during login
Key Tests:
demo_SuccessfulLogin()- Happy pathdemo_FailedLogin()- Error handlingdemo_RotationDuringLogin()- Rotation scenario
File: demo/DataSyncDemo.kt
Shows background sync with:
- Progress updates
- Priority notifications
- Background/foreground transitions
- Database operations
Key Tests:
demo_SuccessfulSync()- Complete sync flowdemo_BackgroundSync_QueuesNotifications()- Background scenariodemo_PriorityNotifications()- Priority handling
File: demo/ErrorHandlingDemo.kt
Shows error handling patterns:
- Network errors with retry
- Validation errors
- Permission errors
- Analytics error logging
Key Tests:
demo_NetworkErrorWithRetry()- Retry mechanismdemo_ValidationErrors()- Form validationdemo_ErrorDuringRotation()- Error + rotation
File: demo/MultiFeatureCoordinationDemo.kt
Shows complex workflows:
- Shopping cart checkout
- Multiple features coordinating
- Sequential operations
- Partial registration
Key Tests:
demo_CompleteCheckoutFlow_Success()- Happy pathdemo_RotationDuringPayment()- Rotation during checkoutdemo_PartialFeatureRegistration()- Gradual registration
@BeforeTest
fun setup() {
KRelay.reset()
KRelayMetrics.reset()
}
@AfterTest
fun tearDown() {
KRelay.reset()
KRelayMetrics.reset()
}// Good
@Test
fun testRotation_PreservesQueuedActions_WhenActivityDestroyed()
// Avoid
@Test
fun test1()@Test
fun testExample() {
// Given: Setup state
val mock = MockTestFeature()
KRelay.register(mock)
// When: Perform action
KRelay.dispatch<TestFeature> { it.execute("test") }
// Then: Verify result
assertEquals(listOf("test"), mock.executedValues)
}@Test
fun testSuccess() { /* happy path */ }
@Test
fun testFailure() { /* error path */ }// Instead of this:
KRelay.reset()
KRelay.debugMode = true
// ... test code ...
KRelay.reset()
// Use this:
withKRelay(debugMode = true) {
// ... test code ...
}Model tests after actual use cases:
- Screen rotations
- Background/foreground transitions
- Network errors
- Permission issues
@Test
fun testWithMetrics() {
KRelayMetrics.reset()
// ... operations ...
assertMetrics<ToastFeature>(
dispatches = 3L,
queued = 2L
)
}Each test should verify one specific behavior:
// Good - focused test
@Test
fun testDispatch_QueuesAction_WhenNotRegistered()
// Avoid - testing multiple things
@Test
fun testEverything()Problem: Tests expecting synchronous execution fail because runOnMain is async.
Solution: Use queue-based tests instead:
// Instead of testing execution directly
@Test
fun testExecution() {
val mock = MockTestFeature()
KRelay.register(mock)
KRelay.dispatch<TestFeature> { it.execute("test") }
assertEquals(listOf("test"), mock.executedValues) // May fail!
}
// Test queue behavior
@Test
fun testQueuing() {
KRelay.dispatch<TestFeature> { it.execute("test") }
assertEquals(1, KRelay.getPendingCount<TestFeature>())
val mock = MockTestFeature()
KRelay.register(mock)
assertEquals(0, KRelay.getPendingCount<TestFeature>()) // Reliable!
}Problem: Metrics show zero despite operations.
Solution: Ensure metrics are not being reset:
@Test
fun testMetrics() {
KRelayMetrics.reset() // Reset at start
// Perform operations
KRelay.dispatch<ToastFeature> { it.show("Test") }
// Don't call reset again before checking!
val metrics = KRelay.getMetrics<ToastFeature>()
assertTrue(metrics["dispatches"]!! > 0)
}Problem: Queue count doesn't go to zero after registration.
Solution: Check if actions are actually being replayed:
// Verify replay happens
KRelay.dispatch<ToastFeature> { it.show("Test") }
assertEquals(1, KRelay.getPendingCount<ToastFeature>())
KRelay.register<ToastFeature>(AndroidToast())
// Queue should be cleared
assertEquals(0, KRelay.getPendingCount<ToastFeature>())Problem: Tests pass individually but fail when run together.
Solution: Ensure proper cleanup in @AfterTest:
@AfterTest
fun tearDown() {
KRelay.reset()
KRelayMetrics.reset()
// Clean any other shared state
}Problem: Priority actions not behaving as expected.
Solution: Remember priority affects queue order, not immediate execution:
// Priority matters when actions are queued
KRelay.dispatchWithPriority<ToastFeature>(ActionPriority.CRITICAL) {
it.show("Critical")
}
// When registered, critical actions replay first
KRelay.register<ToastFeature>(toast)- Unit Tests: 4 files, ~30 test cases
- Integration Tests: 3 files, ~20 test cases
- System Tests: 3 files, ~20 test cases
- Demo Examples: 4 files, ~25 test cases
Total: ~95 test cases covering all major functionality
- ✅ Registration/Unregistration
- ✅ Dispatch (immediate and queued)
- ✅ Queue management
- ✅ Priority system
- ✅ Metrics tracking
- ✅ Weak references
- ✅ Thread safety
- ✅ Action expiry
- ✅ Screen rotation
- ✅ Background/foreground
- ✅ Error handling
- ✅ Multi-feature coordination
- Determine test level (unit/integration/system/demo)
- Create test file in appropriate package
- Follow existing patterns
- Use test utilities for common operations
- Verify test passes in isolation and with full suite
When adding new features:
- Write test first (it should fail)
- Implement feature
- Test should pass
- Add integration/system tests
- Create demo example if it's a major feature
Add to CI pipeline:
# In CI script
./gradlew :krelay:test --continue
./gradlew :krelay:jacocoTestReport
# Fail if coverage < 80%
./gradlew :krelay:jacocoTestCoverageVerificationKRelay provides:
- ✅ Comprehensive test suite (95+ tests)
- ✅ Test utilities for easy testing
- ✅ Demo examples showing real usage
- ✅ Clear testing patterns
- ✅ Full coverage of features
Use this guide to write effective tests and ensure KRelay works perfectly in your app!