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 |