Skip to content

Testing Search

Deepstaging provides TestSearchIndex<T> and TestVectorIndex<T> — generic test doubles for full-text and vector search with call recording, seedable documents, and configurable search responses. No mocking libraries needed.

TestSearchIndex\<T>

Records every IndexAsync, SearchAsync, RemoveAsync, and ClearAsync call. Seed documents and configure search behavior.

var index = new TestSearchIndex<Article>();

// Seed documents
index.Seed("article-1", new Article("Getting Started", "A beginner's guide..."));
index.Seed("article-2", new Article("Advanced Patterns", "Deep dive into..."));

// ... run your handler that searches ...

// Assert search was performed
await Assert.That(index.SearchCalls).Count().IsEqualTo(1);
await Assert.That(index.SearchCalls[0].Query).IsEqualTo("beginner");

Call Recording

Property Type Description
IndexCalls IReadOnlyList<(string Id, T Document)> All IndexAsync calls
IndexManyCalls IReadOnlyList<IReadOnlyDictionary<string, T>> All IndexManyAsync batches
SearchCalls IReadOnlyList<(string Query, SearchOptions? Options)> All search queries
RemoveCalls IReadOnlyList<string> All IDs passed to RemoveAsync
ClearCallCount int Number of ClearAsync calls
Documents IReadOnlyDictionary<string, T> Current index contents

Default Search Behavior

By default, SearchAsync returns all seeded and indexed documents with score 1.0:

var index = new TestSearchIndex<Product>();
index.Seed("p1", new Product("Widget"));
index.Seed("p2", new Product("Gadget"));

var result = await index.SearchAsync("anything");
// Returns both documents as hits with score 1.0

Custom Search Responses

index.OnSearch = (query, options) => new SearchResult<Article>(
    [new SearchHit<Article>("article-1", seededArticle, 0.95)],
    TotalCount: 1);

TestVectorIndex\<T>

Records every UpsertAsync, SearchAsync, RemoveAsync, and ClearAsync call. Seed documents with embeddings and configure search behavior.

var index = new TestVectorIndex<Article>();

// Seed with embeddings
var embedding = new float[] { 0.1f, 0.2f, 0.3f };
index.Seed("article-1", new Article("Getting Started", "..."), embedding);

// ... run your handler that performs vector search ...

await Assert.That(index.SearchCalls).Count().IsEqualTo(1);

Call Recording

Property Type Description
UpsertCalls IReadOnlyList<(string Id, T Document, ReadOnlyMemory<float> Embedding)> All UpsertAsync calls
UpsertManyCalls IReadOnlyList<IReadOnlyList<VectorDocument<T>>> All UpsertManyAsync batches
SearchCalls IReadOnlyList<(ReadOnlyMemory<float> QueryEmbedding, VectorSearchOptions? Options)> All search queries
RemoveCalls IReadOnlyList<string> All IDs passed to RemoveAsync
ClearCallCount int Number of ClearAsync calls
Documents IReadOnlyDictionary<string, (T Document, ReadOnlyMemory<float> Embedding)> Current index contents

Custom Vector Search Responses

index.OnSearch = (embedding, options) => new VectorSearchResult<Article>(
    [new VectorSearchHit<Article>("article-1", seededArticle, 0.98)],
    TotalCount: 1);

Reset Between Tests

index.Reset();       // text search
vectorIndex.Reset(); // vector search