Skip to content

@tdi2/di-testing Overview

Production-ready testing framework that provides Spring Boot-style testing decorators, advanced mocking utilities, and seamless integration with popular testing frameworks.

🧪 Testing Features

  • @DiTest Equivalent - Spring Boot-style test configuration
  • @MockBean Implementation - Advanced service mocking
  • Test Containers - Isolated DI containers for testing
  • Mock Utilities - Comprehensive mocking and stubbing
  • Integration Testing - Full component and service testing

TDI2 testing is built around three core patterns: DI-focused tests (pure service testing), Service Component-focused tests (components with service props), and Behavior Component-focused tests (full integration testing).

1. DI-Focused Tests (Service Unit Testing)

Section titled “1. DI-Focused Tests (Service Unit Testing)”

Test services in complete isolation with simple mocking.

ProductService.test.ts
import { ProductService } from '../services/ProductService';
describe('ProductService', () => {
let productService: ProductService;
let mockRepository: jest.Mocked<ProductRepositoryInterface>;
beforeEach(() => {
mockRepository = {
getAll: jest.fn(),
getById: jest.fn(),
search: jest.fn(),
};
productService = new ProductService(mockRepository);
});
it('should load products successfully', async () => {
// Arrange
const mockProducts = [
{ id: '1', name: 'iPhone', price: 999, category: 'phones' },
{ id: '2', name: 'MacBook', price: 1999, category: 'laptops' }
];
mockRepository.getAll.mockResolvedValue(mockProducts);
// Act
await productService.loadProducts();
// Assert
expect(productService.state.products).toEqual(mockProducts);
expect(productService.state.loading).toBe(false);
expect(productService.state.error).toBe(null);
});
});

Test components with service props using simple mock objects.

ProductList.test.tsx
import { render, screen } from '@testing-library/react';
import { ProductList } from '../components/ProductList';
describe('ProductList', () => {
it('should render products correctly', () => {
// Arrange - Simple mock object
const mockProductService = {
state: {
products: [
{ id: '1', name: 'iPhone', price: 999 },
{ id: '2', name: 'MacBook', price: 1999 }
],
loading: false,
error: null
},
loadProducts: jest.fn(),
setSearchQuery: jest.fn(),
};
// Act
render(<ProductList productService={mockProductService} />);
// Assert
expect(screen.getByText('iPhone')).toBeInTheDocument();
expect(screen.getByText('MacBook')).toBeInTheDocument();
});
it('should show loading state', () => {
// Arrange
const mockProductService = {
state: { products: [], loading: true, error: null },
loadProducts: jest.fn(),
setSearchQuery: jest.fn(),
};
// Act
render(<ProductList productService={mockProductService} />);
// Assert
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});

Test complete component behavior with full DI integration.

ProductCatalog.integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { DIProvider } from '@tdi2/di-core';
import { ProductCatalog } from '../components/ProductCatalog';
import { createTestContainer } from '@tdi2/di-testing';
describe('ProductCatalog Integration', () => {
it('should load and display products', async () => {
// Arrange - Real DI container with test setup
const container = createTestContainer({
productRepository: {
getAll: () => Promise.resolve([
{ id: '1', name: 'iPhone', price: 999 }
])
}
});
// Act
render(
<DIProvider container={container}>
<ProductCatalog />
</DIProvider>
);
// Assert - Full behavior testing
await waitFor(() => {
expect(screen.getByText('iPhone')).toBeInTheDocument();
});
});
});
import { DiTest, MockBean, TestInject } from '@tdi2/di-testing';
@DiTest({
profiles: ['test'],
mockServices: ['EmailService', 'PaymentService']
})
class ProductServiceTest {
@TestInject()
private productService: ProductServiceInterface;
@MockBean()
private emailService: EmailServiceInterface;
@MockBean()
private paymentService: PaymentServiceInterface;
@Test()
async shouldProcessOrderSuccessfully() {
// Arrange
const order = { id: '123', total: 100 };
this.paymentService.processPayment.mockResolvedValue({ success: true });
// Act
const result = await this.productService.processOrder(order);
// Assert
expect(result.success).toBe(true);
expect(this.emailService.sendConfirmation).toHaveBeenCalled();
}
}
@DiTest()
class AdvancedMockingTest {
@MockBean({
scope: 'singleton',
reset: true,
spy: true // Create spy instead of mock
})
private userService: UserServiceInterface;
@MockBean({
autoMock: false // Manual mock setup
})
private paymentService: PaymentServiceInterface;
beforeEach() {
// MockBean automatically resets if reset: true
this.setupPaymentServiceMock();
}
private setupPaymentServiceMock() {
this.paymentService.processPayment = jest.fn().mockImplementation(
async (amount) => ({ success: amount > 0 })
);
}
}

Creates a DI container pre-configured for testing.

import { createTestContainer } from '@tdi2/di-testing';
// Basic test container
const container = createTestContainer();
// Container with specific mocks
const container = createTestContainer({
productRepository: {
getAll: () => Promise.resolve([]),
getById: (id) => Promise.resolve(null),
},
userService: {
state: { currentUser: null },
login: jest.fn(),
logout: jest.fn(),
}
});

Creates a mock service with Jest functions and default state.

import { mockService } from '@tdi2/di-testing';
// Full service mock with Jest functions
const mockProductService = mockService<ProductServiceInterface>({
state: {
products: [],
loading: false,
error: null
}
});
// Mock automatically includes jest.fn() for all methods
expect(mockProductService.loadProducts).toHaveBeenCalled();

Creates repository mocks with data fixtures.

import { createMockRepository } from '@tdi2/di-testing';
// Repository with test data
const mockRepo = createMockRepository<Product>([
{ id: '1', name: 'iPhone', price: 999 },
{ id: '2', name: 'MacBook', price: 1999 }
]);
// Supports standard repository methods
const product = await mockRepo.getById('1');
const allProducts = await mockRepo.getAll();
// Error simulation
mockRepo.simulateError('getById', new Error('Not found'));

Renders components wrapped with test DI container.

import { renderWithDI } from '@tdi2/di-testing';
// Render with default test container
const { getByText } = renderWithDI(<ProductList />);
// Render with specific mocks
const { getByText } = renderWithDI(<ProductList />, {
productService: {
state: { products: [], loading: true }
}
});
Terminal window
# Core testing framework
npm install @tdi2/di-testing
# Or with bun
bun add @tdi2/di-testing
# Testing utilities also included with di-core
npm install @tdi2/di-core
src/__tests__/setup.ts
import '@testing-library/jest-dom';
import { configureTDI2Testing, DiTest, MockBean } from '@tdi2/di-testing';
// Configure global test settings
configureTDI2Testing({
// Global test configuration
defaultMocks: {
// Services that should always be mocked
analyticsService: { track: jest.fn() },
loggerService: { log: jest.fn(), error: jest.fn() }
},
// Automatic cleanup
autoCleanup: true,
// Profile for testing
activeProfiles: ['test'],
// Mock configuration
mockDefaults: {
autoMock: true,
resetBetweenTests: true
}
});
jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
transform: {
'^.+\\.(ts|tsx)$': '@tdi2/jest-transformer',
},
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
}
};
import { DiTest, withProfiles } from '@tdi2/di-testing';
@DiTest({ profiles: ['test', 'integration'] })
class IntegrationTest {
@Test()
async shouldUseTestDatabase() {
// This test runs with test profile
// Automatically uses test-specific services
}
}
// Test different profile configurations
describe('Profile Testing', () => {
it('should use development services',
withProfiles(['development'], async () => {
const container = createTestContainer();
const emailService = container.get('EmailService');
expect(emailService.constructor.name).toBe('MockEmailService');
})
);
it('should use production services',
withProfiles(['production'], async () => {
const container = createTestContainer();
const emailService = container.get('EmailService');
expect(emailService.constructor.name).toBe('SmtpEmailService');
})
);
});
import { DiTest, TestConfiguration, Bean } from '@tdi2/di-testing';
@TestConfiguration()
class TestDatabaseConfig {
@Bean()
createTestDatabase(): DatabaseConnection {
return new InMemoryDatabase();
}
@Bean()
createTestEmailService(): EmailServiceInterface {
return new MockEmailService();
}
}
@DiTest({ configuration: [TestDatabaseConfig] })
class ConfigurationTest {
@TestInject()
private database: DatabaseConnection;
@Test()
async shouldUseTestConfiguration() {
expect(this.database).toBeInstanceOf(InMemoryDatabase);
}
}
  • DI-Focused: Pure service business logic
  • Service Component-Focused: UI rendering with service props
  • Behavior Component-Focused: Full integration testing
// ✅ Good - Simple mock
const mockService = { state: { data: [] }, loadData: jest.fn() };
// ❌ Complex - Overengineered mock
class ComplexMockService extends RealService {
constructor() { /* complex setup */ }
// ... lots of mock implementation
}
// ✅ Good - Test the outcome
expect(cartService.state.total).toBe(1998);
// ❌ Bad - Test implementation details
expect(cartService.calculateTotal).toHaveBeenCalled();
// ✅ Good
it('should show error message when product loading fails')
// ❌ Bad
it('should handle error')
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithDI, DiTest, MockBean } from '@tdi2/di-testing';
@DiTest()
class ComponentIntegrationTest {
@MockBean()
private productService: ProductServiceInterface;
@Test()
async shouldRenderProductList() {
// Setup mock data
this.productService.state = {
products: [{ id: '1', name: 'iPhone' }],
loading: false
};
// Render with DI
const user = userEvent.setup();
const { getByRole } = renderWithDI(<ProductList />);
// Test interactions
await user.click(getByRole('button', { name: /load products/i }));
expect(this.productService.loadProducts).toHaveBeenCalled();
}
}
vitest.config.ts
import { defineConfig } from 'vitest/config';
import { diTestingPlugin } from '@tdi2/di-testing/vite';
export default defineConfig({
plugins: [diTestingPlugin()],
test: {
environment: 'jsdom',
setupFiles: ['./src/__tests__/setup.ts'],
globals: true,
}
});
// Use decorators in Vitest
import { DiTest, MockBean } from '@tdi2/di-testing';
@DiTest()
class VitestExample {
@MockBean()
private service: MyServiceInterface;
@Test()
async shouldWork() {
expect(this.service).toBeDefined();
}
}
@tdi2/di-testing/
├── decorators/ # @DiTest, @MockBean, @TestInject
├── containers/ # Test container implementations
├── mocking/ # Advanced mocking utilities
├── configuration/ # Test configuration classes
├── profiles/ # Profile management for tests
├── utilities/ # Helper functions and test utilities
├── integrations/ # Framework integrations (Jest, Vitest, RTL)
└── types/ # TypeScript type definitions
🎯 Key Takeaway

@tdi2/di-testing provides enterprise-grade testing infrastructure with Spring Boot familiarity. Start with @DiTest and @MockBean for immediate productivity gains.


TDI2’s testing approach focuses on simplicity: choose the right pattern for your test, use simple mocks, and test behavior rather than implementation details.