Skip to main content

Testing Guide

The DTX Portal uses Vitest 3.2.4 with Testing Library React 16.3 and jsdom 27.0.0 for testing. The infrastructure is fully configured — this guide covers how to write and run tests.

Current State

Test coverage is at 0% — the infrastructure is ready but no test files exist yet. This guide documents the patterns to follow when adding tests.


Running Tests

pnpm test           # Run all tests once
pnpm test:ui # Open Vitest visual dashboard (browser-based)
pnpm test -- --watch # Watch mode — re-runs on file changes
pnpm test -- src/components/pipelines/ # Run tests in a specific directory
pnpm test -- PipelineList.test.tsx # Run a single test file

Test File Convention

RuleConvention
LocationSame directory as the source file
Naming{FileName}.test.ts or {FileName}.test.tsx
Globalsdescribe, it, expect, vi are globally available (no imports needed)
components/
pipelines/
PipelineList.tsx
PipelineList.test.tsx ← test file next to source

Vitest Configuration

The test config lives in vitest.config.ts:

SettingValue
Environmentjsdom
GlobalsEnabled (no explicit imports)
Setup filesrc/test/setup.ts
CSSProcessing enabled
Path aliasesSame as tsconfig.json (@/, @dtx/ui, @dtx/utils)

Testing Patterns

1. Component Tests

Render a component, interact with it, assert the output.

// components/pipelines/PipelineList.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PipelineList from './PipelineList';

describe('PipelineList', () => {
it('renders pipeline names', () => {
render(<PipelineList pipelines={mockPipelines} />);

expect(screen.getByText('My Pipeline')).toBeInTheDocument();
expect(screen.getByText('Test Pipeline')).toBeInTheDocument();
});

it('calls onDelete when delete button clicked', async () => {
const onDelete = vi.fn();
render(<PipelineList pipelines={mockPipelines} onDelete={onDelete} />);

await userEvent.click(screen.getByRole('button', { name: /delete/i }));

expect(onDelete).toHaveBeenCalledWith('pipeline-1');
});
});

2. React Query Hooks

Wrap components that use React Query in a test QueryClientProvider.

import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
}

function TestWrapper({ children }: { children: React.ReactNode }) {
const queryClient = createTestQueryClient();
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}

describe('PipelineListPage', () => {
it('shows loading state then data', async () => {
// Mock the service
vi.spyOn(pipelineService, 'getAll').mockResolvedValue(mockPipelines);

render(<PipelineListPage />, { wrapper: TestWrapper });

// Initially loading
expect(screen.getByText(/loading/i)).toBeInTheDocument();

// After data loads
await waitFor(() => {
expect(screen.getByText('My Pipeline')).toBeInTheDocument();
});
});
});

3. Zustand Stores

Test stores directly without rendering components.

import { usePipelineStore } from '@/stores/usePipelineStore';

describe('usePipelineStore', () => {
beforeEach(() => {
// Reset store between tests
usePipelineStore.setState(usePipelineStore.getInitialState());
});

it('adds a node to the canvas', () => {
const { addNode } = usePipelineStore.getState();

addNode({ id: 'node-1', type: 'source', position: { x: 0, y: 0 }, data: {} });

const { nodes } = usePipelineStore.getState();
expect(nodes).toHaveLength(1);
expect(nodes[0].id).toBe('node-1');
});

it('removes a node and its connected edges', () => {
// Setup: add node + edge
usePipelineStore.setState({
nodes: [{ id: 'n1' }, { id: 'n2' }],
edges: [{ id: 'e1', source: 'n1', target: 'n2' }],
});

const { removeNode } = usePipelineStore.getState();
removeNode('n1');

const state = usePipelineStore.getState();
expect(state.nodes).toHaveLength(1);
expect(state.edges).toHaveLength(0);
});
});

4. Service Classes

Mock fetchWithAuth to test service methods without hitting the backend.

import { PipelineService } from '@/services/pipeline.service';

vi.mock('@/services/fetchWithAuth', () => ({
fetchWithAuth: vi.fn(),
}));

import { fetchWithAuth } from '@/services/fetchWithAuth';

describe('PipelineService', () => {
const service = PipelineService.getInstance();

it('creates a pipeline', async () => {
const mockResponse = {
ok: true,
json: () => Promise.resolve({
status: 'success',
data: { id: 'p1', name: 'Test' },
}),
};
vi.mocked(fetchWithAuth).mockResolvedValue(mockResponse);

const result = await service.create({ name: 'Test' });

expect(fetchWithAuth).toHaveBeenCalledWith(
expect.stringContaining('/pipelines'),
expect.objectContaining({ method: 'POST' })
);
expect(result.name).toBe('Test');
});
});

5. Form Validation (React Hook Form + Zod)

Test Zod schemas directly for validation logic.

import { pipelineSchema } from '@/types/pipeline.types';

describe('pipelineSchema', () => {
it('validates a valid pipeline', () => {
const result = pipelineSchema.safeParse({
name: 'My Pipeline',
description: 'Test pipeline',
type: 'PROCESSING',
});

expect(result.success).toBe(true);
});

it('rejects empty name', () => {
const result = pipelineSchema.safeParse({
name: '',
description: 'Test',
type: 'PROCESSING',
});

expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toContain('name');
});
});

Mocking Patterns

Mock Keycloak Auth

vi.mock('@/providers/AuthProvider', () => ({
useAuth: () => ({
token: 'mock-jwt-token',
user: { name: 'Test User', tenantId: 'tenant-1' },
isAuthenticated: true,
}),
}));

Mock Zustand Store

vi.mock('@/stores/usePipelineStore', () => ({
usePipelineStore: vi.fn(() => ({
nodes: mockNodes,
edges: mockEdges,
addNode: vi.fn(),
removeNode: vi.fn(),
})),
}));

Mock API Response

vi.spyOn(operatorService, 'getAll').mockResolvedValue([
{ id: 'op1', name: 'Kafka Source', category: 'SOURCE' },
{ id: 'op2', name: 'Transformer', category: 'PROCESSOR' },
]);

What to Test vs. What to Skip

Test These

WhatWhy
Business logic in hooksCore app behavior — mutations, transformations, validation
Form validation schemasZod schemas define data contracts
Store actionsZustand store logic (add, remove, update operations)
Service methodsVerify correct API calls and response handling
Complex component interactionsMulti-step wizards, conditional rendering
Error statesVerify error messages display correctly

Skip These

WhatWhy
@dtx/ui base componentsThey wrap React Aria — already tested by the library
Static pagesNo logic to test — visual review is sufficient
Styling / layoutTailwind classes don't need unit tests
Third-party librariesXYFlow, Monaco, Recharts — trust the library
Trivial componentsComponents that just pass props down

Test Utilities

Create a shared test setup in src/test/:

FilePurpose
setup.tsGlobal test setup (jsdom, cleanup, mock globals)
test-utils.tsxCustom render() with providers (QueryClient, Auth, Router)
mocks/Shared mock data (mockPipeline, mockOperator, mockUser)

Custom Render with Providers

// src/test/test-utils.tsx
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router';

function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});

return (
<QueryClientProvider client={queryClient}>
<MemoryRouter>
{children}
</MemoryRouter>
</QueryClientProvider>
);
}

export function renderWithProviders(ui: React.ReactElement) {
return render(ui, { wrapper: AllProviders });
}

Usage:

import { renderWithProviders } from '@/test/test-utils';

it('renders pipeline page', () => {
renderWithProviders(<PipelineListPage />);
// ...
});