Typesense Testing Guide: SDK Testing, Collection CRUD, and Search Ranking

Typesense Testing Guide: SDK Testing, Collection CRUD, and Search Ranking

Typesense is gaining ground as a fast, developer-friendly alternative to Algolia and Elasticsearch. Testing Typesense integrations well means covering three layers: the SDK client calls, the collection schema operations, and the search result assertions. This guide walks through all three.

Typesense Testing Strategies

There are two main approaches to testing Typesense:

  1. Mock the SDK client — fast, isolated unit tests that verify your code sends the right operations
  2. Run Typesense in a container — integration tests against a real Typesense instance

Both are valuable. Start with mocks for fast feedback, then add container-based tests for critical paths.

Setting Up Mock Tests

Install the Typesense client and Jest:

npm install --save-dev typesense jest

Create a mock factory:

// test-utils/typesense-mock.js
export function createMockTypesenseClient() {
  const collections = new Map();

  return {
    collections: {
      create: jest.fn(async (schema) => {
        collections.set(schema.name, { schema, documents: [] });
        return schema;
      }),
      retrieve: jest.fn(async () => {
        return Array.from(collections.values()).map(c => c.schema);
      }),
    },
    collections: jest.fn((name) => ({
      documents: {
        create: jest.fn(async (doc) => {
          const collection = collections.get(name);
          if (collection) collection.documents.push(doc);
          return doc;
        }),
        search: jest.fn(async (searchParams) => ({
          found: 0,
          hits: [],
          search_time_ms: 1,
          page: 1,
          ...searchParams._mockResponse,
        })),
        delete: jest.fn(async (id) => ({ id })),
        upsert: jest.fn(async (doc) => doc),
      },
      retrieve: jest.fn(async () => collections.get(name)?.schema),
      delete: jest.fn(async () => ({ name })),
    })),
  };
}

Testing Collection CRUD Operations

Collection schema management is foundational. Test that your app creates collections with the right schema:

import { createMockTypesenseClient } from '../test-utils/typesense-mock';
import { ProductSearchService } from '../services/product-search';

describe('ProductSearchService', () => {
  let client;
  let service;

  beforeEach(() => {
    client = createMockTypesenseClient();
    service = new ProductSearchService(client);
  });

  test('creates collection with correct schema on init', async () => {
    await service.initialize();

    expect(client.collections.create).toHaveBeenCalledWith({
      name: 'products',
      fields: [
        { name: 'name', type: 'string' },
        { name: 'price', type: 'float' },
        { name: 'category', type: 'string', facet: true },
        { name: 'rating', type: 'float' },
        { name: 'in_stock', type: 'bool' },
      ],
      default_sorting_field: 'rating',
    });
  });

  test('indexes a product document', async () => {
    await service.initialize();
    const product = {
      id: 'prod-123',
      name: 'Running Shoes',
      price: 129.99,
      category: 'footwear',
      rating: 4.5,
      in_stock: true,
    };

    await service.indexProduct(product);

    const collection = client.collections('products');
    expect(collection.documents.create).toHaveBeenCalledWith(product);
  });

  test('deletes a product by id', async () => {
    await service.initialize();
    await service.deleteProduct('prod-123');

    const collection = client.collections('products');
    expect(collection.documents.delete).toHaveBeenCalledWith('prod-123');
  });
});

Testing Search Queries

The most important test layer: verify your search parameters are correct:

describe('search operations', () => {
  test('sends correct search parameters for basic query', async () => {
    const client = createMockTypesenseClient();
    const service = new ProductSearchService(client);
    await service.initialize();

    const mockSearchResponse = {
      found: 2,
      hits: [
        {
          document: { id: '1', name: 'Running Shoes', price: 99 },
          highlight: { name: { snippet: '<mark>Running</mark> Shoes' } },
          text_match: 135242178,
        },
      ],
      search_time_ms: 2,
    };

    client.collections('products').documents.search
      .mockResolvedValueOnce(mockSearchResponse);

    const results = await service.search('running shoes');

    expect(client.collections('products').documents.search).toHaveBeenCalledWith({
      q: 'running shoes',
      query_by: 'name,description',
      per_page: 20,
      page: 1,
    });

    expect(results.found).toBe(2);
    expect(results.hits[0].document.name).toBe('Running Shoes');
  });

  test('applies price range filter correctly', async () => {
    const client = createMockTypesenseClient();
    const service = new ProductSearchService(client);
    await service.initialize();

    client.collections('products').documents.search
      .mockResolvedValueOnce({ found: 1, hits: [], search_time_ms: 1 });

    await service.search('shoes', { minPrice: 50, maxPrice: 150 });

    expect(client.collections('products').documents.search).toHaveBeenCalledWith(
      expect.objectContaining({
        filter_by: 'price:>=50 && price:<=150',
      })
    );
  });

  test('sorts by price ascending', async () => {
    const client = createMockTypesenseClient();
    const service = new ProductSearchService(client);
    await service.initialize();

    client.collections('products').documents.search
      .mockResolvedValueOnce({ found: 0, hits: [], search_time_ms: 1 });

    await service.search('shoes', { sortBy: 'price:asc' });

    expect(client.collections('products').documents.search).toHaveBeenCalledWith(
      expect.objectContaining({
        sort_by: 'price:asc',
      })
    );
  });
});

Facets are critical for e-commerce and catalog search. Test both the request and the response handling:

test('requests facets for category and brand', async () => {
  const client = createMockTypesenseClient();
  const service = new ProductSearchService(client);
  await service.initialize();

  const facetedResponse = {
    found: 10,
    hits: [],
    facet_counts: [
      {
        field_name: 'category',
        counts: [
          { value: 'footwear', count: 5 },
          { value: 'apparel', count: 3 },
        ],
        stats: {},
      },
    ],
    search_time_ms: 3,
  };

  client.collections('products').documents.search
    .mockResolvedValueOnce(facetedResponse);

  const results = await service.searchWithFacets('shoes', ['category', 'brand']);

  expect(client.collections('products').documents.search).toHaveBeenCalledWith(
    expect.objectContaining({
      facet_by: 'category,brand',
    })
  );

  expect(results.facets.category).toEqual([
    { value: 'footwear', count: 5 },
    { value: 'apparel', count: 3 },
  ]);
});

Testing Search Ranking Assertions

Typesense lets you customize ranking via sort_by and pinned_hits. Test that your ranking rules produce the expected order:

test('pins promoted products to top of results', async () => {
  const client = createMockTypesenseClient();
  const service = new ProductSearchService(client);
  await service.initialize();

  const promotedIds = ['prod-featured-1', 'prod-featured-2'];
  
  client.collections('products').documents.search
    .mockResolvedValueOnce({
      found: 5,
      hits: [
        { document: { id: 'prod-featured-1', name: 'Featured Shoes' } },
        { document: { id: 'prod-featured-2', name: 'Featured Boots' } },
        { document: { id: 'prod-3', name: 'Regular Sneakers' } },
      ],
      search_time_ms: 2,
    });

  await service.search('footwear', { pinnedHits: promotedIds });

  expect(client.collections('products').documents.search).toHaveBeenCalledWith(
    expect.objectContaining({
      pinned_hits: 'prod-featured-1:1,prod-featured-2:2',
    })
  );
});

Integration Tests with Testcontainers

For critical paths, run tests against a real Typesense instance:

import { GenericContainer, Wait } from 'testcontainers';
import Typesense from 'typesense';

describe('Typesense integration', () => {
  let container;
  let client;

  beforeAll(async () => {
    container = await new GenericContainer('typesense/typesense:26.0')
      .withExposedPorts(8108)
      .withEnvironment({ TYPESENSE_DATA_DIR: '/data', TYPESENSE_API_KEY: 'test-key' })
      .withWaitStrategy(Wait.forHttp('/health', 8108))
      .start();

    client = new Typesense.Client({
      nodes: [{
        host: container.getHost(),
        port: container.getMappedPort(8108),
        protocol: 'http',
      }],
      apiKey: 'test-key',
      connectionTimeoutSeconds: 2,
    });
  }, 60_000);

  afterAll(async () => {
    await container?.stop();
  });

  test('creates collection and searches documents', async () => {
    await client.collections().create({
      name: 'test-products',
      fields: [
        { name: 'name', type: 'string' },
        { name: 'price', type: 'float' },
      ],
      default_sorting_field: 'price',
    });

    await client.collections('test-products').documents().create({
      id: '1',
      name: 'Running Shoes',
      price: 99.99,
    });

    const results = await client.collections('test-products').documents().search({
      q: 'running',
      query_by: 'name',
    });

    expect(results.found).toBe(1);
    expect(results.hits[0].document.name).toBe('Running Shoes');
  });

  test('returns empty results for no matches', async () => {
    const results = await client.collections('test-products').documents().search({
      q: 'xxxxxxnotfound',
      query_by: 'name',
    });

    expect(results.found).toBe(0);
    expect(results.hits).toHaveLength(0);
  });
});

Testing Upsert Operations

Typesense's upsert is important for keeping your search index in sync with your database:

test('upserts existing document without error', async () => {
  const client = createMockTypesenseClient();
  const service = new ProductSearchService(client);
  await service.initialize();

  const product = { id: 'prod-1', name: 'Shoes', price: 99, category: 'footwear', rating: 4, in_stock: true };
  await service.indexProduct(product);

  // Now update the price
  const updatedProduct = { ...product, price: 89 };
  await service.indexProduct(updatedProduct, { upsert: true });

  expect(client.collections('products').documents.upsert).toHaveBeenCalledWith(updatedProduct);
});

E2E Search Testing with HelpMeTest

Unit tests catch bugs in your Typesense integration logic, but they don't verify the full user experience. HelpMeTest monitors your search UI end-to-end:

Navigate to the product catalog at https://your-app.com/products
Type "running shoes" in the search bar
Verify search results appear within 2 seconds
Verify results contain "shoes" in the product names
Click "Footwear" in the category facet
Verify results count decreases to show filtered results
Clear filters
Verify original result count is restored

Run these 24/7 to catch Typesense downtime, schema changes, or API key rotation issues before users notice.

Summary

Testing Typesense thoroughly requires:

  1. Mock client tests for fast, isolated verification of query parameters and schema operations
  2. Collection CRUD tests to ensure your schema matches what Typesense expects
  3. Facet and filter tests to verify complex query construction
  4. Ranking tests to ensure promoted content and sort orders work
  5. Testcontainers integration tests for real behavior validation
  6. E2E monitoring via HelpMeTest to catch production search regressions

The mock-first approach keeps your test suite fast while container tests provide confidence in production behavior.

Read more