#cqrs #event-sourcing #event-store #ddd #event-sourcing-cqrs

yanked eventcore-memory

In-memory adapter for EventCore event sourcing library (for testing)

5 releases

0.1.8 Jul 23, 2025
0.1.7 Jul 22, 2025
0.1.5 Jul 21, 2025
0.1.4 Jul 21, 2025
0.1.3 Jul 9, 2025

#35 in #event-sourcing-cqrs

21 downloads per month
Used in union_square

MIT/Apache

1MB
24K SLoC

eventcore-memory

In-memory event store adapter for EventCore - perfect for testing and development.

Features

  • Zero setup - No database required
  • Thread-safe - Safe for concurrent testing
  • Fast - No I/O overhead
  • Deterministic - Consistent test results
  • Full API compatibility - Drop-in replacement

Installation

[dev-dependencies]
eventcore-memory = "0.1"

Usage in Tests

use eventcore_memory::MemoryEventStore;
use eventcore::{CommandExecutor, testing::*};

#[tokio::test]
async fn test_my_command() {
    // Create store - no setup needed!
    let store = MemoryEventStore::new();
    let executor = CommandExecutor::new(store);
    
    // Test your commands
    let result = executor.execute(MyCommand { ... }).await;
    assert!(result.is_ok());
}

Test Patterns

Given-When-Then Testing

use eventcore::testing::CommandTestHarness;

#[tokio::test]
async fn transfer_should_move_money() {
    CommandTestHarness::new()
        .given_events(vec![
            AccountOpened { id: "alice", balance: Money::new(1000) },
            AccountOpened { id: "bob", balance: Money::new(0) },
        ])
        .when(TransferMoney { 
            from: "alice", 
            to: "bob", 
            amount: Money::new(100) 
        })
        .then_expect_events(vec![
            MoneyWithdrawn { account: "alice", amount: Money::new(100) },
            MoneyDeposited { account: "bob", amount: Money::new(100) },
        ])
        .run()
        .await
        .unwrap();
}

Testing Concurrency

#[tokio::test]
async fn concurrent_transfers_should_not_overdraw() {
    let store = MemoryEventStore::new();
    let executor = CommandExecutor::new(store);
    
    // Setup account
    executor.execute(OpenAccount { 
        id: "alice", 
        initial: Money::new(100) 
    }).await.unwrap();
    
    // Try concurrent transfers
    let transfer1 = executor.execute(TransferMoney {
        from: "alice", to: "bob", amount: Money::new(60)
    });
    
    let transfer2 = executor.execute(TransferMoney {
        from: "alice", to: "charlie", amount: Money::new(60)
    });
    
    let (result1, result2) = tokio::join!(transfer1, transfer2);
    
    // One should succeed, one should fail
    assert!(result1.is_ok() ^ result2.is_ok());
}

Testing Projections

#[tokio::test]
async fn projection_should_track_balances() {
    let store = MemoryEventStore::new();
    let mut projection = BalanceProjection::new();
    
    // Apply events
    let events = vec![
        AccountOpened { id: "alice", balance: Money::new(1000) },
        MoneyWithdrawn { account: "alice", amount: Money::new(100) },
    ];
    
    for event in events {
        projection.apply(&event).await.unwrap();
    }
    
    // Check projection state
    assert_eq!(projection.balance("alice"), Money::new(900));
}

Limitations

The memory adapter is for testing only:

  • ❌ No persistence - data lost on restart
  • ❌ No distributed transactions
  • ❌ Limited concurrency control compared to PostgreSQL
  • ❌ No query capabilities beyond basic operations

For production, use eventcore-postgres.

Advanced Testing

Snapshot Testing

#[test]
fn test_with_snapshot() {
    let store = MemoryEventStore::new();
    
    // Take snapshot
    let snapshot = store.snapshot();
    
    // Make changes
    store.append_events(...).await.unwrap();
    
    // Restore snapshot
    store.restore(snapshot);
    
    // Store is back to original state
}

Chaos Testing

#[test]
async fn test_random_failures() {
    let store = MemoryEventStore::with_chaos(ChaosConfig {
        failure_rate: 0.1,  // 10% chance of failure
        latency: Some(Duration::from_millis(100)),
    });
    
    // Test your error handling
}

Performance Testing

#[test]
async fn benchmark_command_throughput() {
    let store = MemoryEventStore::new();
    let executor = CommandExecutor::new(store);
    
    let start = Instant::now();
    for i in 0..10_000 {
        executor.execute(TestCommand { id: i }).await.unwrap();
    }
    let elapsed = start.elapsed();
    
    println!("Commands/sec: {}", 10_000.0 / elapsed.as_secs_f64());
}

Integration with Test Frameworks

With rstest

use rstest::*;

#[fixture]
fn event_store() -> MemoryEventStore {
    MemoryEventStore::new()
}

#[fixture]
fn executor(event_store: MemoryEventStore) -> CommandExecutor<MemoryEventStore> {
    CommandExecutor::new(event_store)
}

#[rstest]
#[tokio::test]
async fn test_with_fixtures(executor: CommandExecutor<MemoryEventStore>) {
    // Your test here
}

With proptest

use proptest::prelude::*;

proptest! {
    #[test]
    fn transfer_properties(
        amount in 1..1000u64,
        initial in 1000..10000u64
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let store = MemoryEventStore::new();
            // Property-based testing
        });
    }
}

Debugging

Enable trace logging to see all operations:

#[test]
fn debug_test() {
    let _ = env_logger::builder()
        .filter_level(log::LevelFilter::Trace)
        .try_init();
        
    let store = MemoryEventStore::new();
    // All operations will be logged
}

See Also

Dependencies

~10–15MB
~265K SLoC