Skip to content

Testing Data Store

Every [DataStore] entity automatically generates a Test{Entity}Store — a test double that implements the store interface with call recording, seedable state, and typed query helpers. No mocking libraries needed.

Generated Test Store

For this entity:

[TypedId]
public readonly partial struct ArticleId;

[StoredEntity]
public partial record Article(ArticleId Id, string Title, string Body);

[DataStore]
public static partial class AppStore;

The generator produces TestArticleStore alongside InMemoryArticleStore:

[DevelopmentOnly]
public class TestArticleStore : IArticleStore, IStoreQueryExecutor<Article>
{
    // Call recording
    public IReadOnlyList<Article> SaveCalls => _saveCalls;
    public IReadOnlyList<IReadOnlyList<Article>> SaveManyCalls => _saveManyCalls;
    public IReadOnlyList<ArticleId> DeleteCalls => _deleteCalls;
    public IReadOnlyList<ArticleId> GetByIdCalls => _getByIdCalls;
    // ... plus DeleteManyCalls, GetByIdsCalls

    // Current state
    public IReadOnlyDictionary<ArticleId, Article> Entities => _store;

    // Seeding (not recorded as SaveCalls)
    public void Seed(Article article) { ... }
    public void Seed(params Article[] articles) { ... }

    // Clear everything
    public void Reset() { ... }
}

Usage in Tests

Seed and Assert

var store = new TestArticleStore();

// Pre-populate
var article = new Article(ArticleId.New(), "Hello World", "First post");
store.Seed(article);

// ... run your command handler ...

// Assert saves
await Assert.That(store.SaveCalls).Count().IsEqualTo(1);
await Assert.That(store.SaveCalls[0].Title).IsEqualTo("Updated Title");

// Assert deletes
await Assert.That(store.DeleteCalls).Contains(article.Id);

// Check current state
await Assert.That(store.Entities).Count().IsEqualTo(0);

With TestRuntime

var store = new TestArticleStore();
store.Seed(existingArticle);

var runtime = TestAppRuntime.CreateConfigured();
// Wire store into runtime's IHasAppStore capability

var program = AppDispatch.Dispatch(new UpdateArticle(articleId, "New Title"));
await program.RunAsync(runtime);

await Assert.That(store.SaveCalls).Count().IsEqualTo(1);

Call Recording

Property Type Description
SaveCalls IReadOnlyList<T> Entities passed to SaveAsync
SaveManyCalls IReadOnlyList<IReadOnlyList<T>> Batches passed to SaveManyAsync
DeleteCalls IReadOnlyList<TKey> Keys passed to DeleteAsync
DeleteManyCalls IReadOnlyList<IEnumerable<TKey>> Key batches passed to DeleteManyAsync
GetByIdCalls IReadOnlyList<TKey> Keys passed to GetByIdAsync
GetByIdsCalls IReadOnlyList<IEnumerable<TKey>> Key batches passed to GetByIdsAsync
Entities IReadOnlyDictionary<TKey, T> Current store contents

Seeding

Seed adds entities to the store without recording them as SaveCalls. This lets you distinguish between pre-existing data and data written by your handler:

var store = new TestArticleStore();

// These are background data — not recorded
store.Seed(
    new Article(id1, "First", "..."),
    new Article(id2, "Second", "..."));

// This is what your handler does — IS recorded
await store.SaveAsync(new Article(id3, "Third", "..."));

await Assert.That(store.SaveCalls).Count().IsEqualTo(1);
await Assert.That(store.Entities).Count().IsEqualTo(3);

Query Support

TestStore implements IStoreQueryExecutor<T>, so the composable StoreQuery<T> API works exactly as in production:

var store = new TestArticleStore();
store.Seed(articles);

var result = await store.Query()
    .Where(a => a.Title.Contains("Guide"))
    .OrderBy(a => a.Title)
    .ToPageAsync(page: 1, pageSize: 10);

await Assert.That(result.Data).Count().IsGreaterThan(0);

Test Store vs InMemory Store

Both are generated per entity. They serve different purposes:

InMemoryStore TestStore
Purpose Run your app without a database Assert behavior in unit tests
Call recording No Yes — SaveCalls, DeleteCalls, etc.
Seeding No Yes — Seed() without recording
Reset No Yes — Reset() clears everything
DI default Registered by Add{Store}() Manual registration in tests

Reset Between Tests

store.Reset(); // Clears entities, all call recording lists