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
  • Test framework installed (Vitest or Jest)
  • Basic understanding of lists and queries

Unit Testing Queries

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

Basic Query Test

product-queries.spec.ts
import { describe, it, expect, beforeEach } 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();
  });

  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

filter-combinations.spec.ts
import { describe, it, expect, beforeEach } 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' });
  });

  it('should combine category and brand filters (AND)', () => {
    // Filter: Apple products in Phones category
    list.catalog.query.apply({
      groupKey: 'categoryFilter',
      attribute: 'category',
      operator: 'eq',
      value: 'Phones',
    });

    list.catalog.query.apply({
      groupKey: 'brandFilter',
      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.apply({
      groupKey: 'categoryFilter',
      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.apply({
      groupKey: 'categoryFilter',
      attribute: 'category',
      operator: 'eq',
      value: 'Phones',
    });

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

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

    // Remove only category filter
    list.catalog.query.remove({ groupKey: 'categoryFilter' });

    // 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

sorting-pagination.spec.ts
import { describe, it, expect, beforeEach } 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' });
  });

  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

test-helpers/mock-dcupl.ts
import { vi } from 'vitest';

export interface MockDcuplList {
  catalog: {
    query: {
      items: ReturnType<typeof vi.fn>;
      count: ReturnType<typeof vi.fn>;
      apply: ReturnType<typeof vi.fn>;
      clear: ReturnType<typeof vi.fn>;
      remove: ReturnType<typeof vi.fn>;
    };
    fn: {
      facets: ReturnType<typeof vi.fn>;
      aggregate: ReturnType<typeof vi.fn>;
      metadata: ReturnType<typeof vi.fn>;
    };
    filters: {
      get: ReturnType<typeof vi.fn>;
      getAll: ReturnType<typeof vi.fn>;
    };
  };
  destroy: ReturnType<typeof vi.fn>;
}

export interface MockDcupl {
  models: {
    set: ReturnType<typeof vi.fn>;
    get: ReturnType<typeof vi.fn>;
  };
  data: {
    set: ReturnType<typeof vi.fn>;
  };
  lists: {
    create: ReturnType<typeof vi.fn>;
    get: ReturnType<typeof vi.fn>;
    getAll: ReturnType<typeof vi.fn>;
  };
  init: ReturnType<typeof vi.fn>;
  destroy: ReturnType<typeof vi.fn>;
}

export function createMockList<T>(items: T[] = []): MockDcuplList {
  return {
    catalog: {
      query: {
        items: vi.fn().mockReturnValue(items),
        count: vi.fn().mockReturnValue(items.length),
        apply: vi.fn(),
        clear: vi.fn(),
        remove: vi.fn(),
      },
      fn: {
        facets: vi.fn().mockReturnValue([]),
        aggregate: vi.fn().mockReturnValue(0),
        metadata: vi.fn().mockReturnValue({
          key: 'TestModel',
          currentSize: items.length,
          initialSize: items.length,
          appliedQuery: { queries: [] },
        }),
      },
      filters: {
        get: vi.fn().mockReturnValue(null),
        getAll: 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(),
    },
    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(undefined),
    destroy: vi.fn(),
  };
}

Testing a React Component with Mock

ProductList.spec.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ProductList } from './ProductList';
import { createMockDcupl, createMockList } from '../test-helpers/mock-dcupl';

// Mock the dcupl module
vi.mock('@dcupl/core', () => ({
  Dcupl: vi.fn(),
}));

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]]));
  });

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

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

  it('should call query.apply when filtering', () => {
    render(<ProductList dcupl={mockDcupl as MockDcupl} />);

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

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

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

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

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

Testing an Angular Component with Mock

product-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductListComponent } from './product-list.component';
import { DcuplService } from '../services/dcupl.service';
import { createMockDcupl, createMockList } from '../test-helpers/mock-dcupl';

describe('ProductListComponent', () => {
  let component: ProductListComponent;
  let fixture: ComponentFixture<ProductListComponent>;
  let mockDcuplService: jasmine.SpyObj<DcuplService>;

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

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

    mockDcuplService = jasmine.createSpyObj('DcuplService', ['getDcupl', 'isReady']);
    mockDcuplService.getDcupl.and.returnValue(mockDcupl as MockDcupl);
    mockDcuplService.isReady.and.returnValue(true);

    await TestBed.configureTestingModule({
      imports: [ProductListComponent],
      providers: [{ provide: DcuplService, useValue: mockDcuplService }],
    }).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', () => {
    const mockList = mockDcuplService.getDcupl().lists.create({ modelKey: 'Product' });

    component.filterByCategory('Electronics');

    expect(mockList.catalog.query.apply).toHaveBeenCalled();
  });
});

Integration Testing

Test complete workflows with real dcupl instances.

Full Workflow Test

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
    list.catalog.query.apply({
      groupKey: 'categoryFilter',
      attribute: 'category.key',
      operator: 'eq',
      value: 'cat1',
    });

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

    // Step 3: Verify facets update
    const updatedFacets = list.catalog.fn.facets({ attribute: 'category' });
    const electronicsFacet = updatedFacets.find((f) => f.value?.key === 'cat1');
    expect(electronicsFacet?.count).toBe(2);

    // Step 4: Add price filter
    list.catalog.query.apply({
      groupKey: 'priceFilter',
      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
    dcupl.data.set([{ key: 'p6', name: 'Monitor', price: 399, category: 'cat1', inStock: true }], {
      model: 'Product',
      mode: 'add',
    });

    await dcupl.update();
    list.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', () => {
    // Create a list pre-filtered to only electronics
    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

aggregations.integration.spec.ts
import { describe, it, expect, beforeEach } 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();
  });

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

    const totalQuantity = list.catalog.fn.aggregate({
      attribute: 'quantity',
      aggregation: 'sum',
    });

    expect(totalQuantity).toBe(11);
  });

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

    const avgPrice = list.catalog.fn.aggregate({
      attribute: 'price',
      aggregation: 'avg',
    });

    expect(avgPrice).toBe(376.5);
  });

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

    const minPrice = list.catalog.fn.aggregate({
      attribute: 'price',
      aggregation: 'min',
    });

    const maxPrice = list.catalog.fn.aggregate({
      attribute: 'price',
      aggregation: 'max',
    });

    expect(minPrice).toBe(29);
    expect(maxPrice).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 completedTotal = list.catalog.fn.aggregate({
      attribute: 'quantity',
      aggregation: 'sum',
    });

    expect(completedTotal).toBe(8); // 2 + 5 + 1
  });
});

Test Data Strategies

Using Fixtures

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

test-helpers/factories.ts
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 });
}

// Usage in tests
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

test-setup.ts
import { beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';

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));
  await dcuplInstance.init();
  return dcuplInstance;
}

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

// Use in test files
describe('My Tests', () => {
  beforeEach(async () => {
    await setupTestDcupl([productModel, categoryModel]);
  });

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

  it('should work', () => {
    const dcupl = getTestDcupl();
    // ... test code
  });
});

Best Practices

Arrange-Act-Assert pattern:

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);
});

Each test should be independent:

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

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

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

  // Tests are now isolated
});

Be specific in assertions:

// 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',
});