# How to test dcupl applications

Write reliable tests for applications that use the dcupl SDK, covering unit tests for queries, component tests with mocks, and integration tests.

## Prerequisites

- Working dcupl application
- Vitest installed. The examples import from `vitest` and use `vi`; with Jest, use `jest.fn()` for `vi.fn()` and Jest's globals instead of the imports
- Basic understanding of [lists](/sdk/core-concepts/lists) and [queries](/sdk/queries/query-filter)

## Unit testing queries

Test query logic in isolation using real dcupl instances with inline test data.

### Basic query test

```typescript filename="product-queries.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';

describe('Product Queries', () => {
  let dcupl: Dcupl;

  const productModel: ModelDefinition = {
    key: 'Product',
    properties: [
      { key: 'name', type: 'string' },
      { key: 'price', type: 'float' },
      { key: 'category', type: 'string', filter: true },
      { key: 'inStock', type: 'boolean' },
    ],
    data: [
      { key: 'p1', name: 'Laptop', price: 999, category: 'Electronics', inStock: true },
      { key: 'p2', name: 'Mouse', price: 29, category: 'Electronics', inStock: true },
      { key: 'p3', name: 'Desk', price: 299, category: 'Furniture', inStock: false },
      { key: 'p4', name: 'Chair', price: 199, category: 'Furniture', inStock: true },
    ],
  };

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set(productModel);
    await dcupl.init();
  });

  afterEach(() => {
    dcupl.destroy();
  });

  it('should return all products', () => {
    const list = dcupl.lists.create({ modelKey: 'Product' });
    const items = list.catalog.query.items();

    expect(items).toHaveLength(4);
  });

  it('should filter by category', () => {
    const list = dcupl.lists.create({ modelKey: 'Product' });

    list.catalog.query.apply({
      attribute: 'category',
      operator: 'eq',
      value: 'Electronics',
    });

    const items = list.catalog.query.items();

    expect(items).toHaveLength(2);
    expect(items.every((item) => item.category === 'Electronics')).toBe(true);
  });

  it('should filter by price range', () => {
    const list = dcupl.lists.create({ modelKey: 'Product' });

    list.catalog.query.apply({
      groupKey: 'priceRange',
      groupType: 'and',
      queries: [
        { attribute: 'price', operator: 'gte', value: 100 },
        { attribute: 'price', operator: 'lte', value: 500 },
      ],
    });

    const items = list.catalog.query.items();

    expect(items).toHaveLength(2);
    expect(items.map((i) => i.name)).toContain('Desk');
    expect(items.map((i) => i.name)).toContain('Chair');
  });
});
```

### Testing filter combinations

```typescript filename="filter-combinations.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';

describe('Filter Combinations', () => {
  let dcupl: Dcupl;
  let list: ReturnType<typeof dcupl.lists.create>;

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set({
      key: 'Product',
      properties: [
        { key: 'name', type: 'string' },
        { key: 'category', type: 'string', filter: true },
        { key: 'brand', type: 'string', filter: true },
        { key: 'price', type: 'float' },
      ],
      data: [
        { key: 'p1', name: 'iPhone', category: 'Phones', brand: 'Apple', price: 999 },
        { key: 'p2', name: 'Galaxy', category: 'Phones', brand: 'Samsung', price: 899 },
        { key: 'p3', name: 'MacBook', category: 'Laptops', brand: 'Apple', price: 1299 },
        { key: 'p4', name: 'ThinkPad', category: 'Laptops', brand: 'Lenovo', price: 999 },
      ],
    });
    await dcupl.init();
    list = dcupl.lists.create({ modelKey: 'Product' });
  });

  afterEach(() => {
    dcupl.destroy();
  });

  it('should combine category and brand filters (AND)', () => {
    // Filter: Apple products in Phones category
    // Each condition goes into the group keyed by its attribute; different attributes combine with AND
    list.catalog.query.addCondition({
      attribute: 'category',
      operator: 'eq',
      value: 'Phones',
    });

    list.catalog.query.addCondition({
      attribute: 'brand',
      operator: 'eq',
      value: 'Apple',
    });

    const items = list.catalog.query.items();

    expect(items).toHaveLength(1);
    expect(items[0].name).toBe('iPhone');
  });

  it('should support OR within a filter group', () => {
    // Filter: Apple OR Samsung products
    list.catalog.query.apply({
      groupKey: 'brandFilter',
      groupType: 'or',
      queries: [
        { attribute: 'brand', operator: 'eq', value: 'Apple' },
        { attribute: 'brand', operator: 'eq', value: 'Samsung' },
      ],
    });

    const items = list.catalog.query.items();

    expect(items).toHaveLength(3);
  });

  it('should clear filters correctly', () => {
    list.catalog.query.addCondition({
      attribute: 'category',
      operator: 'eq',
      value: 'Phones',
    });

    expect(list.catalog.query.items()).toHaveLength(2);

    list.catalog.query.clear();

    expect(list.catalog.query.items()).toHaveLength(4);
  });

  it('should remove specific filter group', () => {
    list.catalog.query.addCondition({
      attribute: 'category',
      operator: 'eq',
      value: 'Phones',
    });

    list.catalog.query.addCondition({
      attribute: 'brand',
      operator: 'eq',
      value: 'Apple',
    });

    expect(list.catalog.query.items()).toHaveLength(1);

    // Remove only the category filter (its group is keyed by the attribute name)
    list.catalog.query.removeGroup('category');

    // Should now show all Apple products
    const items = list.catalog.query.items();
    expect(items).toHaveLength(2);
    expect(items.every((i) => i.brand === 'Apple')).toBe(true);
  });
});
```

### Testing sorting and pagination

```typescript filename="sorting-pagination.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';

describe('Sorting and Pagination', () => {
  let dcupl: Dcupl;
  let list: ReturnType<typeof dcupl.lists.create>;

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set({
      key: 'Product',
      properties: [
        { key: 'name', type: 'string' },
        { key: 'price', type: 'float' },
        { key: 'rating', type: 'float' },
      ],
      data: [
        { key: 'p1', name: 'Product A', price: 100, rating: 4.5 },
        { key: 'p2', name: 'Product B', price: 50, rating: 4.8 },
        { key: 'p3', name: 'Product C', price: 150, rating: 4.2 },
        { key: 'p4', name: 'Product D', price: 75, rating: 4.6 },
      ],
    });
    await dcupl.init();
    list = dcupl.lists.create({ modelKey: 'Product' });
  });

  afterEach(() => {
    dcupl.destroy();
  });

  it('should sort by price ascending', () => {
    const items = list.catalog.query.items({
      sort: { attributes: ['price'], order: ['ASC'] },
    });

    expect(items[0].price).toBe(50);
    expect(items[3].price).toBe(150);
  });

  it('should sort by price descending', () => {
    const items = list.catalog.query.items({
      sort: { attributes: ['price'], order: ['DESC'] },
    });

    expect(items[0].price).toBe(150);
    expect(items[3].price).toBe(50);
  });

  it('should paginate results', () => {
    const page1 = list.catalog.query.items({ start: 0, count: 2 });
    const page2 = list.catalog.query.items({ start: 2, count: 2 });

    expect(page1).toHaveLength(2);
    expect(page2).toHaveLength(2);
    expect(page1[0].key).not.toBe(page2[0].key);
  });

  it('should combine sorting and pagination', () => {
    const items = list.catalog.query.items({
      sort: { attributes: ['price'], order: ['ASC'] },
      start: 1,
      count: 2,
    });

    expect(items).toHaveLength(2);
    expect(items[0].price).toBe(75); // Second cheapest
    expect(items[1].price).toBe(100); // Third cheapest
  });
});
```

## Mocking dcupl for component tests

When testing UI components, mock dcupl to avoid initializing the full SDK.

### Creating a mock dcupl instance

The mock needs every method your component calls. This one covers the common query, function and data methods; add the ones your components use.

```typescript filename="test-helpers/mock-dcupl.ts"
import { vi, type Mock } from 'vitest';

export interface MockDcuplList {
  catalog: {
    query: {
      items: Mock;
      first: Mock;
      count: Mock;
      addCondition: Mock;
      setCondition: Mock;
      removeCondition: Mock;
      addGroup: Mock;
      removeGroup: Mock;
      apply: Mock;
      clear: Mock;
      remove: Mock;
    };
    fn: {
      facets: Mock;
      aggregate: Mock;
      metadata: Mock;
    };
    filters: {
      get: Mock;
      getAll: Mock;
    };
  };
  on: Mock;
  onResultChange: Mock;
  destroy: Mock;
}

export interface MockDcupl {
  models: {
    set: Mock;
    get: Mock;
  };
  data: {
    set: Mock;
    update: Mock;
    upsert: Mock;
    remove: Mock;
  };
  lists: {
    create: Mock;
    get: Mock;
    getAll: Mock;
  };
  init: Mock;
  update: Mock;
  destroy: Mock;
}

export function createMockList<T>(items: T[] = []): MockDcuplList {
  return {
    catalog: {
      query: {
        items: vi.fn().mockReturnValue(items),
        first: vi.fn().mockReturnValue(items[0]),
        count: vi.fn().mockReturnValue(items.length),
        addCondition: vi.fn(),
        setCondition: vi.fn(),
        removeCondition: vi.fn(),
        addGroup: vi.fn(),
        removeGroup: vi.fn(),
        apply: vi.fn(),
        clear: vi.fn(),
        remove: vi.fn(),
      },
      fn: {
        facets: vi.fn().mockReturnValue([]),
        aggregate: vi.fn().mockReturnValue({ attribute: 'price', types: ['sum'], sum: 0 }),
        metadata: vi.fn().mockReturnValue({
          key: 'TestModel',
          currentSize: items.length,
          initialSize: items.length,
          appliedQuery: { queries: [] },
        }),
      },
      filters: {
        get: vi.fn().mockReturnValue(undefined),
        getAll: vi.fn().mockReturnValue([]),
      },
    },
    on: vi.fn().mockReturnValue(() => {}), // returns an unsubscribe function, like the real one
    onResultChange: vi.fn().mockReturnValue(() => {}),
    destroy: vi.fn(),
  };
}

export function createMockDcupl(lists: Map<string, MockDcuplList> = new Map()): MockDcupl {
  return {
    models: {
      set: vi.fn(),
      get: vi.fn(),
    },
    data: {
      set: vi.fn(),
      update: vi.fn(),
      upsert: vi.fn(),
      remove: vi.fn(),
    },
    lists: {
      create: vi.fn().mockImplementation(({ modelKey }) => {
        return lists.get(modelKey) || createMockList();
      }),
      get: vi.fn().mockImplementation((key) => lists.get(key)),
      getAll: vi.fn().mockReturnValue(Array.from(lists.values())),
    },
    init: vi.fn().mockResolvedValue(true), // the real init() resolves true, or false on failure
    update: vi.fn().mockResolvedValue(true),
    destroy: vi.fn(),
  };
}
```

### Testing a React component with mock

The component under test gets the instance as a prop, creates its list, and filters by category:

```tsx filename="ProductList.tsx"
import { useEffect, useState } from 'react';
import type { Dcupl, DcuplList } from '@dcupl/core';

type Product = { key: string; name: string; category: string };

export function ProductList({ dcupl }: { dcupl: Dcupl }) {
  const [list, setList] = useState<DcuplList | null>(null);
  const [items, setItems] = useState<Product[]>([]);

  useEffect(() => {
    const productList = dcupl.lists.create({ modelKey: 'Product' });
    setList(productList);
    setItems(productList.catalog.query.items<Product>());
    return () => productList.destroy();
  }, [dcupl]);

  const filterByCategory = (category: string) => {
    list?.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: category });
    setItems(list?.catalog.query.items<Product>() ?? []);
  };

  const clearFilters = () => {
    list?.catalog.query.clear();
    setItems(list?.catalog.query.items<Product>() ?? []);
  };

  return (
    <div>
      <button onClick={() => filterByCategory('Electronics')}>Electronics</button>
      <button onClick={clearFilters}>Clear filters</button>
      <ul>
        {items.map((product) => (
          <li key={product.key}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}
```

The test passes the mock in place of a `Dcupl` instance. The mock has only part of the `Dcupl` type, so cast it with `as unknown as Dcupl`. `toBeInTheDocument()` comes from `@testing-library/jest-dom`: import `@testing-library/jest-dom/vitest` in your Vitest setup file and run the tests with `environment: 'jsdom'`; for `tsc` to know the matcher, list the setup file in your `tsconfig.json` `include`.

```tsx filename="ProductList.spec.tsx"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
import type { Dcupl } from '@dcupl/core';
import { ProductList } from './ProductList';
import { createMockDcupl, createMockList } from './test-helpers/mock-dcupl';

describe('ProductList Component', () => {
  const mockProducts = [
    { key: 'p1', name: 'Laptop', price: 999, category: 'Electronics' },
    { key: 'p2', name: 'Mouse', price: 29, category: 'Electronics' },
    { key: 'p3', name: 'Desk', price: 299, category: 'Furniture' },
  ];

  let mockList: ReturnType<typeof createMockList>;
  let mockDcupl: ReturnType<typeof createMockDcupl>;

  beforeEach(() => {
    mockList = createMockList(mockProducts);
    mockDcupl = createMockDcupl(new Map([['Product', mockList]]));
  });

  afterEach(() => {
    cleanup(); // unmount; Testing Library does this on its own only with Vitest's globals enabled
  });

  it('should render product list', () => {
    render(<ProductList dcupl={mockDcupl as unknown as Dcupl} />);

    expect(screen.getByText('Laptop')).toBeInTheDocument();
    expect(screen.getByText('Mouse')).toBeInTheDocument();
    expect(screen.getByText('Desk')).toBeInTheDocument();
  });

  it('should set the category condition when filtering', () => {
    render(<ProductList dcupl={mockDcupl as unknown as Dcupl} />);

    const filterButton = screen.getByRole('button', { name: /electronics/i });
    fireEvent.click(filterButton);

    expect(mockList.catalog.query.setCondition).toHaveBeenCalledWith(
      expect.objectContaining({
        attribute: 'category',
        operator: 'eq',
        value: 'Electronics',
      }),
    );
  });

  it('should call query.clear when clearing filters', () => {
    render(<ProductList dcupl={mockDcupl as unknown as Dcupl} />);

    const clearButton = screen.getByRole('button', { name: /clear/i });
    fireEvent.click(clearButton);

    expect(mockList.catalog.query.clear).toHaveBeenCalled();
  });
});
```

### Testing an Angular component with mock

Here the instance comes from a service of your own, which the test replaces:

```typescript filename="services/dcupl.service.ts"
import { Injectable } from '@angular/core';
import { Dcupl } from '@dcupl/core';

@Injectable({ providedIn: 'root' })
export class DcuplService {
  private readonly dcupl = new Dcupl();

  getDcupl(): Dcupl {
    return this.dcupl;
  }
}
```

```typescript filename="product-list.component.ts"
import { Component, inject } from '@angular/core';
import { DcuplService } from './services/dcupl.service';

type Product = { key: string; name: string };

@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products; track product.key) {
      <div class="product-item">{{ product.name }}</div>
    }
  `,
})
export class ProductListComponent {
  private readonly list = inject(DcuplService).getDcupl().lists.create({ modelKey: 'Product' });
  products = this.list.catalog.query.items<Product>();

  filterByCategory(category: string) {
    this.list.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: category });
    this.products = this.list.catalog.query.items<Product>();
  }
}
```

```typescript filename="product-list.component.spec.ts"
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductListComponent } from './product-list.component';
import { DcuplService } from './services/dcupl.service';
import { createMockDcupl, createMockList, type MockDcuplList } from './test-helpers/mock-dcupl';

describe('ProductListComponent', () => {
  let component: ProductListComponent;
  let fixture: ComponentFixture<ProductListComponent>;
  let mockList: MockDcuplList;

  const mockProducts = [
    { key: 'p1', name: 'Laptop', price: 999 },
    { key: 'p2', name: 'Mouse', price: 29 },
  ];

  beforeEach(async () => {
    mockList = createMockList(mockProducts);
    const mockDcupl = createMockDcupl(new Map([['Product', mockList]]));

    await TestBed.configureTestingModule({
      imports: [ProductListComponent],
      providers: [{ provide: DcuplService, useValue: { getDcupl: vi.fn().mockReturnValue(mockDcupl) } }],
    }).compileComponents();

    fixture = TestBed.createComponent(ProductListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should display products', () => {
    const productElements = fixture.nativeElement.querySelectorAll('.product-item');
    expect(productElements.length).toBe(2);
  });

  it('should filter products when category selected', () => {
    component.filterByCategory('Electronics');

    expect(mockList.catalog.query.setCondition).toHaveBeenCalledWith(
      expect.objectContaining({ attribute: 'category', value: 'Electronics' }),
    );
  });
});
```

## Integration testing

Test complete workflows with real dcupl instances.

### Full workflow test

```typescript filename="product-catalog.integration.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';

describe('Product Catalog Integration', () => {
  let dcupl: Dcupl;

  const categoryModel: ModelDefinition = {
    key: 'Category',
    properties: [{ key: 'name', type: 'string' }],
    data: [
      { key: 'cat1', name: 'Electronics' },
      { key: 'cat2', name: 'Furniture' },
      { key: 'cat3', name: 'Clothing' },
    ],
  };

  const productModel: ModelDefinition = {
    key: 'Product',
    properties: [
      { key: 'name', type: 'string' },
      { key: 'price', type: 'float' },
      { key: 'inStock', type: 'boolean' },
    ],
    references: [{ key: 'category', type: 'singleValued', model: 'Category', filter: true }],
    data: [
      { key: 'p1', name: 'Laptop', price: 999, category: 'cat1', inStock: true },
      { key: 'p2', name: 'Mouse', price: 29, category: 'cat1', inStock: true },
      { key: 'p3', name: 'Desk', price: 299, category: 'cat2', inStock: false },
      { key: 'p4', name: 'Chair', price: 199, category: 'cat2', inStock: true },
      { key: 'p5', name: 'T-Shirt', price: 25, category: 'cat3', inStock: true },
    ],
  };

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set(categoryModel);
    dcupl.models.set(productModel);
    await dcupl.init();
  });

  afterEach(() => {
    dcupl.destroy();
  });

  it('should complete a full filter workflow', () => {
    const list = dcupl.lists.create({ modelKey: 'Product' });

    // Step 1: Get initial facets
    const categoryFacets = list.catalog.fn.facets({ attribute: 'category' });
    expect(categoryFacets).toHaveLength(3);

    // Step 2: Apply category filter (a reference matches by key)
    list.catalog.query.addCondition({
      attribute: 'category',
      operator: 'eq',
      value: 'cat1',
    });

    const filteredItems = list.catalog.query.items();
    expect(filteredItems).toHaveLength(2);

    // Step 3: Verify facets update
    // The facet leaves out the group of its own attribute, so all 3 categories keep their counts
    const updatedFacets = list.catalog.fn.facets({ attribute: 'category' });
    const electronicsFacet = updatedFacets.find((f) => f.key === 'cat1');
    expect(updatedFacets).toHaveLength(3);
    expect(electronicsFacet?.size).toBe(2);
    expect(electronicsFacet?.selected).toBe(true);

    // Step 4: Add price filter
    list.catalog.query.addCondition({
      attribute: 'price',
      operator: 'lte',
      value: 100,
    });

    const priceFilteredItems = list.catalog.query.items();
    expect(priceFilteredItems).toHaveLength(1);
    expect(priceFilteredItems[0].name).toBe('Mouse');

    // Step 5: Clear all filters
    list.catalog.query.clear();
    expect(list.catalog.query.count()).toBe(5);
  });

  it('should handle data updates correctly', async () => {
    const list = dcupl.lists.create({ modelKey: 'Product' });

    // Initial count
    expect(list.catalog.query.count()).toBe(5);

    // Add new product (upsert adds unknown keys; set() would replace all Product items)
    dcupl.data.upsert([{ key: 'p6', name: 'Monitor', price: 399, category: 'cat1', inStock: true }], {
      model: 'Product',
    });

    // Data calls are staged until update(); the list sees the new item on its next read
    await dcupl.update();

    expect(list.catalog.query.count()).toBe(6);

    // Verify the new product is queryable
    list.catalog.query.apply({
      attribute: 'name',
      operator: 'find',
      value: 'Monitor',
    });

    const items = list.catalog.query.items();
    expect(items).toHaveLength(1);
    expect(items[0].price).toBe(399);
  });

  it('should create filtered lists', () => {
    // A list scoped to electronics: it keeps the keys the query selects now, with their current items
    const query = dcupl.query.generate('Product', {
      attribute: 'category.key',
      operator: 'eq',
      value: 'cat1',
    });

    const electronicsList = dcupl.lists.create({ modelKey: 'Product', query });

    const meta = electronicsList.catalog.fn.metadata();
    expect(meta.initialSize).toBe(2);
    expect(meta.currentSize).toBe(2);

    // Additional filters work on the pre-filtered set
    electronicsList.catalog.query.apply({
      attribute: 'price',
      operator: 'lt',
      value: 100,
    });

    expect(electronicsList.catalog.query.count()).toBe(1);
  });
});
```

### Testing with aggregations

```typescript filename="aggregations.integration.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';

describe('Aggregations', () => {
  let dcupl: Dcupl;

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set({
      key: 'Order',
      properties: [
        { key: 'product', type: 'string' },
        { key: 'quantity', type: 'int' },
        { key: 'price', type: 'float' },
        { key: 'status', type: 'string', filter: true },
      ],
      data: [
        { key: 'o1', product: 'Laptop', quantity: 2, price: 999, status: 'completed' },
        { key: 'o2', product: 'Mouse', quantity: 5, price: 29, status: 'completed' },
        { key: 'o3', product: 'Keyboard', quantity: 3, price: 79, status: 'pending' },
        { key: 'o4', product: 'Monitor', quantity: 1, price: 399, status: 'completed' },
      ],
    });
    await dcupl.init();
  });

  afterEach(() => {
    dcupl.destroy();
  });

  it('should calculate sum', () => {
    const list = dcupl.lists.create({ modelKey: 'Order' });

    // aggregate() always needs `types` and returns one object with a field per type
    const result = list.catalog.fn.aggregate({
      attribute: 'quantity',
      types: ['sum'],
    });

    expect(result.sum).toBe(11);
  });

  it('should calculate average', () => {
    const list = dcupl.lists.create({ modelKey: 'Order' });

    const result = list.catalog.fn.aggregate({
      attribute: 'price',
      types: ['avg'],
    });

    expect(result.avg).toBe(376.5);
  });

  it('should calculate min and max', () => {
    const list = dcupl.lists.create({ modelKey: 'Order' });

    const result = list.catalog.fn.aggregate({
      attribute: 'price',
      types: ['min', 'max'],
    });

    expect(result.min).toBe(29);
    expect(result.max).toBe(999);
  });

  it('should aggregate filtered results', () => {
    const list = dcupl.lists.create({ modelKey: 'Order' });

    list.catalog.query.apply({
      attribute: 'status',
      operator: 'eq',
      value: 'completed',
    });

    const completed = list.catalog.fn.aggregate({
      attribute: 'quantity',
      types: ['sum'],
    });

    expect(completed.sum).toBe(8); // 2 + 5 + 1
  });
});
```

## Test data strategies

### Using fixtures

```typescript filename="test-fixtures/products.ts"
import type { ModelDefinition } from '@dcupl/common';

export const PRODUCT_MODEL: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'price', type: 'float' },
    { key: 'category', type: 'string', filter: true },
    { key: 'brand', type: 'string', filter: true },
    { key: 'inStock', type: 'boolean' },
    { key: 'rating', type: 'float' },
  ],
};

export const SAMPLE_PRODUCTS = [
  {
    key: 'p1',
    name: 'Laptop Pro',
    price: 1299,
    category: 'Electronics',
    brand: 'TechCo',
    inStock: true,
    rating: 4.5,
  },
  {
    key: 'p2',
    name: 'Wireless Mouse',
    price: 49,
    category: 'Electronics',
    brand: 'TechCo',
    inStock: true,
    rating: 4.2,
  },
  {
    key: 'p3',
    name: 'Office Desk',
    price: 399,
    category: 'Furniture',
    brand: 'HomeStyle',
    inStock: false,
    rating: 4.0,
  },
  {
    key: 'p4',
    name: 'Ergonomic Chair',
    price: 299,
    category: 'Furniture',
    brand: 'HomeStyle',
    inStock: true,
    rating: 4.8,
  },
  {
    key: 'p5',
    name: 'USB-C Hub',
    price: 79,
    category: 'Electronics',
    brand: 'ConnectPro',
    inStock: true,
    rating: 4.3,
  },
];

export function createProductModel(data = SAMPLE_PRODUCTS): ModelDefinition {
  return {
    ...PRODUCT_MODEL,
    data,
  };
}
```

### Factory functions for test data

```typescript filename="test-helpers/factories.ts"
export interface Product {
  key: string;
  name: string;
  price: number;
  category: string;
  brand: string;
  inStock: boolean;
  rating: number;
}

let idCounter = 0;

export function createProduct(overrides: Partial<Product> = {}): Product {
  idCounter++;
  return {
    key: `product-${idCounter}`,
    name: `Test Product ${idCounter}`,
    price: 100 + idCounter * 10,
    category: 'General',
    brand: 'TestBrand',
    inStock: true,
    rating: 4.0,
    ...overrides,
  };
}

export function createProducts(count: number, overrides: Partial<Product> = {}): Product[] {
  return Array.from({ length: count }, () => createProduct(overrides));
}

export function createProductsInCategory(category: string, count: number): Product[] {
  return createProducts(count, { category });
}
```

Keep `describe` blocks in spec files: a helper module that contains one registers it again in every test file that imports the helper.

```typescript filename="product-volume.spec.ts"
import { describe, it, expect } from 'vitest';
import { Dcupl } from '@dcupl/core';
import { createProducts } from './test-helpers/factories';

describe('Product Tests', () => {
  it('should handle many products', async () => {
    const dcupl = new Dcupl();
    dcupl.models.set({
      key: 'Product',
      properties: [
        { key: 'name', type: 'string' },
        { key: 'price', type: 'float' },
        { key: 'category', type: 'string', filter: true },
      ],
      data: createProducts(1000),
    });

    await dcupl.init();

    const list = dcupl.lists.create({ modelKey: 'Product' });
    expect(list.catalog.query.count()).toBe(1000);
  });
});
```

### Resetting state between tests

```typescript filename="test-helpers/test-dcupl.ts"
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';

let dcuplInstance: Dcupl | null = null;

export function getTestDcupl(): Dcupl {
  if (!dcuplInstance) {
    throw new Error('Dcupl not initialized. Call setupTestDcupl first.');
  }
  return dcuplInstance;
}

export async function setupTestDcupl(models: ModelDefinition[]): Promise<Dcupl> {
  dcuplInstance = new Dcupl();
  models.forEach((model) => dcuplInstance!.models.set(model));
  // init() resolves false instead of throwing, so fail the test setup explicitly
  if (!(await dcuplInstance.init())) {
    throw new Error('dcupl init failed');
  }
  return dcuplInstance;
}

export function cleanupTestDcupl(): void {
  if (dcuplInstance) {
    dcuplInstance.destroy();
    dcuplInstance = null;
  }
}
```

Use the helpers in a spec file (here with the model from the fixtures above):

```typescript filename="my-tests.spec.ts"
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { setupTestDcupl, cleanupTestDcupl, getTestDcupl } from './test-helpers/test-dcupl';
import { createProductModel } from './test-fixtures/products';

describe('My Tests', () => {
  beforeEach(async () => {
    await setupTestDcupl([createProductModel()]);
  });

  afterEach(() => {
    cleanupTestDcupl();
  });

  it('should work', () => {
    const dcupl = getTestDcupl();
    const list = dcupl.lists.create({ modelKey: 'Product' });
    expect(list.catalog.query.count()).toBe(5);
  });
});
```

## Testing data quality

`init()` resolves `true` even when the data has quality errors: a value that doesn't fit its type (`WrongDataType`) or a reference to a key that doesn't exist (`RemoteReferenceKeyNotFound`) doesn't fail it. A test that only checks `init()` misses broken data, so assert on `dcupl.quality.errors.summary()` too:

```typescript filename="product-data-quality.spec.ts"
import { describe, it, expect, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
import { createProductModel } from './test-fixtures/products';

describe('Product data quality', () => {
  let dcupl: Dcupl;

  afterEach(() => {
    dcupl.destroy();
  });

  it('loads the fixtures without quality errors', async () => {
    dcupl = new Dcupl(); // quality checks are on by default
    dcupl.models.set(createProductModel());

    expect(await dcupl.init()).toBe(true);
    expect(dcupl.quality.errors.summary()).toEqual([]);
  });

  it('reports a wrong type and a broken reference', async () => {
    dcupl = new Dcupl();
    dcupl.models.set({
      key: 'Category',
      properties: [{ key: 'name', type: 'string' }],
      data: [{ key: 'cat1', name: 'Electronics' }],
    });
    dcupl.models.set({
      key: 'Product',
      properties: [{ key: 'price', type: 'float' }],
      references: [{ key: 'category', type: 'singleValued', model: 'Category' }],
      data: [
        { key: 'p1', price: 'abc', category: 'cat1' },
        { key: 'p2', price: 10, category: 'cat9' },
      ],
    });

    // init() still resolves true
    expect(await dcupl.init()).toBe(true);

    const errorTypes = dcupl.quality.errors.summary().map((group) => group.errorType);
    expect(errorTypes).toEqual(expect.arrayContaining(['WrongDataType', 'RemoteReferenceKeyNotFound']));
  });
});
```

Each summary entry carries `model`, `attribute`, `errorType` and `totalCount`, so a test can also pin the exact errors it expects. See [Quality & validation](/sdk/advanced/quality-validation) for the error types.

## Best practices

### Test Structure

**Arrange-Act-Assert pattern:**

```typescript
it('should filter by category', () => {
  // Arrange
  const list = dcupl.lists.create({ modelKey: 'Product' });

  // Act
  list.catalog.query.apply({
    attribute: 'category',
    operator: 'eq',
    value: 'Electronics',
  });
  const items = list.catalog.query.items();

  // Assert
  expect(items).toHaveLength(2);
  expect(items.every((i) => i.category === 'Electronics')).toBe(true);
});
```

### Isolation

**Each test should be independent:**

```typescript
import { describe, beforeEach, afterEach } from 'vitest';
import { Dcupl, type DcuplList } from '@dcupl/core';
import { createProductModel } from './test-fixtures/products';

describe('Filter Tests', () => {
  let dcupl: Dcupl;
  let list: DcuplList;

  beforeEach(async () => {
    // Fresh instance for each test
    dcupl = new Dcupl();
    dcupl.models.set(createProductModel());
    await dcupl.init();
    list = dcupl.lists.create({ modelKey: 'Product' });
  });

  afterEach(() => {
    // Clean up
    list.destroy();
    dcupl.destroy();
  });

  // Tests are now isolated
});
```

### Assertions

**Be specific in assertions:**

```typescript
// Bad - too vague
expect(items.length).toBeGreaterThan(0);

// Good - specific expectation
expect(items).toHaveLength(3);

// Good - verify actual content
expect(items.map((i) => i.key)).toEqual(['p1', 'p2', 'p3']);

// Good - verify properties
expect(items[0]).toMatchObject({
  name: 'Laptop',
  category: 'Electronics',
});
```

## Next steps

- [Debugging with Console](/sdk/guides/debugging) — Inspect queries at runtime
- [TypeScript Guide](/sdk/guides/typescript) — Type-safe testing patterns
- [React Integration](/learn/tutorials/react-integration) — Using dcupl in a React app
