Comprehensive testing approaches for applications built with the Asterisms JS SDK Backend.
Testing is crucial for maintaining reliable applications. The Asterisms JS SDK Backend provides patterns and utilities to make testing easier and more effective across unit tests, integration tests, and end-to-end tests.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
coverage: {
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test/', '**/*.test.ts', '**/*.spec.ts']
}
}
});// src/test/setup.ts
import { beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
// Global test utilities
global.testSDK = null;
global.testUser = null;
beforeAll(async () => {
// Global setup
console.log('Setting up test environment...');
});
afterAll(async () => {
// Global cleanup
console.log('Cleaning up test environment...');
});
beforeEach(async () => {
// Reset state before each test
global.testSDK = null;
global.testUser = null;
});
afterEach(async () => {
// Cleanup after each test
if (global.testSDK) {
// Clean up any test data
}
});// src/test/utils/mock-sdk.ts
import { vi } from 'vitest';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export function createMockSDK(overrides: Partial<AsterismsBackendSDK> = {}): AsterismsBackendSDK {
const mockStorage = {
get: vi.fn(),
set: vi.fn(),
list: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
exists: vi.fn()
};
const mockAuth = {
login: vi.fn(),
logout: vi.fn(),
validateToken: vi.fn(),
hasPermission: vi.fn(),
getCurrentUser: vi.fn(),
refreshToken: vi.fn()
};
const mockRegistration = {
getAppInfo: vi.fn(),
updateCapabilities: vi.fn()
};
const mockNotifications = {
send: vi.fn(),
sendBulk: vi.fn(),
getTemplates: vi.fn(),
createTemplate: vi.fn()
};
const mockSDK = {
storage: () => mockStorage,
authorization: () => mockAuth,
registration: () => mockRegistration,
notifications: () => mockNotifications,
boot: vi.fn(),
isBooted: vi.fn().mockReturnValue(true),
...overrides
};
return mockSDK as AsterismsBackendSDK;
}// src/test/utils/test-data.ts
import { faker } from '@faker-js/faker';
export const TestDataFactory = {
user: (overrides = {}) => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: faker.helpers.arrayElement(['admin', 'user', 'viewer']),
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
...overrides
}),
product: (overrides = {}) => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
description: faker.commerce.productDescription(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
...overrides
}),
notification: (overrides = {}) => ({
id: faker.string.uuid(),
title: faker.lorem.sentence(),
message: faker.lorem.paragraph(),
type: faker.helpers.arrayElement(['info', 'warning', 'error', 'success']),
recipient: faker.internet.email(),
createdAt: faker.date.recent().toISOString(),
...overrides
})
};// src/lib/services/UserService.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { UserService } from './UserService';
import { createMockSDK } from '../../test/utils/mock-sdk';
import { TestDataFactory } from '../../test/utils/test-data';
describe('UserService', () => {
let userService: UserService;
let mockSDK: ReturnType<typeof createMockSDK>;
beforeEach(() => {
mockSDK = createMockSDK();
userService = new UserService(mockSDK);
});
describe('findAll', () => {
it('should return all users', async () => {
const mockUsers = [TestDataFactory.user(), TestDataFactory.user(), TestDataFactory.user()];
mockSDK.storage().list.mockResolvedValue(mockUsers);
const result = await userService.findAll();
expect(mockSDK.storage().list).toHaveBeenCalledWith('users');
expect(result).toEqual(mockUsers);
});
it('should handle storage errors', async () => {
const error = new Error('Storage error');
mockSDK.storage().list.mockRejectedValue(error);
await expect(userService.findAll()).rejects.toThrow('Storage error');
});
});
describe('findById', () => {
it('should return user by id', async () => {
const mockUser = TestDataFactory.user();
mockSDK.storage().get.mockResolvedValue(mockUser);
const result = await userService.findById(mockUser.id);
expect(mockSDK.storage().get).toHaveBeenCalledWith('users', mockUser.id);
expect(result).toEqual(mockUser);
});
it('should return null for non-existent user', async () => {
mockSDK.storage().get.mockResolvedValue(null);
const result = await userService.findById('non-existent-id');
expect(result).toBeNull();
});
});
describe('create', () => {
it('should create new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
role: 'user' as const
};
const createdUser = TestDataFactory.user(userData);
mockSDK.storage().create.mockResolvedValue(createdUser);
const result = await userService.create(userData);
expect(mockSDK.storage().create).toHaveBeenCalledWith(
'users',
expect.objectContaining({
email: userData.email,
name: userData.name,
role: userData.role,
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
expect(result).toEqual(createdUser);
});
});
describe('findByEmail', () => {
it('should find user by email', async () => {
const mockUsers = [
TestDataFactory.user({ email: 'user1@example.com' }),
TestDataFactory.user({ email: 'user2@example.com' }),
TestDataFactory.user({ email: 'user3@example.com' })
];
mockSDK.storage().list.mockResolvedValue(mockUsers);
const result = await userService.findByEmail('user2@example.com');
expect(result).toEqual(mockUsers[1]);
});
it('should return null if user not found', async () => {
mockSDK.storage().list.mockResolvedValue([]);
const result = await userService.findByEmail('nonexistent@example.com');
expect(result).toBeNull();
});
});
});// src/lib/controllers/UserController.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { UserController } from './UserController';
import { UserService } from '../services/UserService';
import { createMockSDK } from '../../test/utils/mock-sdk';
import { TestDataFactory } from '../../test/utils/test-data';
// Mock UserService
vi.mock('../services/UserService');
describe('UserController', () => {
let controller: UserController;
let mockSDK: ReturnType<typeof createMockSDK>;
let mockUserService: vi.Mocked<UserService>;
let mockEvent: any;
beforeEach(() => {
mockSDK = createMockSDK();
mockUserService = new UserService(mockSDK) as vi.Mocked<UserService>;
controller = new UserController(mockSDK);
// Replace the service instance
(controller as any).userService = mockUserService;
mockEvent = {
locals: {
sdk: mockSDK,
user: TestDataFactory.user({ role: 'admin' })
},
request: {
json: vi.fn()
},
params: {}
};
});
describe('getUsers', () => {
it('should return users for authenticated user', async () => {
const mockUsers = [TestDataFactory.user(), TestDataFactory.user()];
mockSDK.authorization().hasPermission.mockResolvedValue(true);
mockUserService.findAll.mockResolvedValue(mockUsers);
const response = await controller.getUsers(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(200);
expect(responseData.users).toEqual(mockUsers);
});
it('should return 401 for unauthenticated user', async () => {
mockEvent.locals.user = null;
const response = await controller.getUsers(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(401);
expect(responseData.error).toBe('Unauthorized');
});
it('should return 403 for insufficient permissions', async () => {
mockSDK.authorization().hasPermission.mockResolvedValue(false);
const response = await controller.getUsers(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(403);
expect(responseData.error).toContain('Missing permission');
});
});
describe('createUser', () => {
it('should create user successfully', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'user' as const
};
const createdUser = TestDataFactory.user(userData);
mockEvent.request.json.mockResolvedValue(userData);
mockSDK.authorization().hasPermission.mockResolvedValue(true);
mockUserService.findByEmail.mockResolvedValue(null);
mockUserService.create.mockResolvedValue(createdUser);
const response = await controller.createUser(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(201);
expect(responseData.user).toEqual(createdUser);
});
it('should return 409 for duplicate email', async () => {
const userData = {
email: 'existing@example.com',
name: 'Existing User',
role: 'user' as const
};
const existingUser = TestDataFactory.user({ email: userData.email });
mockEvent.request.json.mockResolvedValue(userData);
mockSDK.authorization().hasPermission.mockResolvedValue(true);
mockUserService.findByEmail.mockResolvedValue(existingUser);
const response = await controller.createUser(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(409);
expect(responseData.error).toBe('User already exists');
});
});
});// src/routes/api/users/+server.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { GET, POST, PUT, DELETE } from './+server';
import { createMockSDK } from '../../../test/utils/mock-sdk';
import { TestDataFactory } from '../../../test/utils/test-data';
describe('/api/users', () => {
let mockSDK: ReturnType<typeof createMockSDK>;
let mockEvent: any;
beforeEach(() => {
mockSDK = createMockSDK();
mockEvent = {
locals: {
sdk: mockSDK,
user: TestDataFactory.user({ role: 'admin' })
},
request: {
json: vi.fn()
},
params: {}
};
});
describe('GET /api/users', () => {
it('should return users list', async () => {
const mockUsers = [TestDataFactory.user(), TestDataFactory.user()];
mockSDK.authorization().hasPermission.mockResolvedValue(true);
mockSDK.storage().list.mockResolvedValue(mockUsers);
const response = await GET(mockEvent);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.users).toEqual(mockUsers);
});
});
describe('POST /api/users', () => {
it('should create new user', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'user'
};
const createdUser = TestDataFactory.user(userData);
mockEvent.request.json.mockResolvedValue(userData);
mockSDK.authorization().hasPermission.mockResolvedValue(true);
mockSDK.storage().list.mockResolvedValue([]); // No existing users
mockSDK.storage().create.mockResolvedValue(createdUser);
const response = await POST(mockEvent);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.user).toEqual(createdUser);
});
});
});// src/test/integration/storage.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
import { createFetchAdapter } from '@asterisms/sdk-backend/adapters/fetch';
import { TestDataFactory } from '../utils/test-data';
describe('Storage Integration', () => {
let sdk: AsterismsBackendSDK;
let testData: any[] = [];
beforeEach(async () => {
sdk = getAsterismsBackendSDK({
bundleId: 'com.test.integration',
domain: 'test.asterisms.local',
subdomain: 'test',
httpServiceAdapterFactory: createFetchAdapter(fetch),
registrationData: {
bundleId: 'com.test.integration',
capabilities: ['BACKEND_SERVICE'],
description: 'Integration Test App',
name: 'Test App',
subdomain: 'test',
title: 'Test App'
}
});
await sdk.boot();
testData = [];
});
afterEach(async () => {
// Clean up test data
const storage = sdk.storage();
for (const item of testData) {
try {
await storage.delete('test_users', item.id);
} catch (error) {
// Ignore cleanup errors
}
}
});
it('should create and retrieve user', async () => {
const storage = sdk.storage();
const userData = TestDataFactory.user();
const createdUser = await storage.create('test_users', userData);
testData.push(createdUser);
const retrievedUser = await storage.get('test_users', createdUser.id);
expect(retrievedUser).toEqual(createdUser);
});
it('should list users', async () => {
const storage = sdk.storage();
const user1 = TestDataFactory.user();
const user2 = TestDataFactory.user();
const created1 = await storage.create('test_users', user1);
const created2 = await storage.create('test_users', user2);
testData.push(created1, created2);
const users = await storage.list('test_users');
expect(users).toContainEqual(created1);
expect(users).toContainEqual(created2);
});
it('should update user', async () => {
const storage = sdk.storage();
const userData = TestDataFactory.user();
const createdUser = await storage.create('test_users', userData);
testData.push(createdUser);
const updates = { name: 'Updated Name' };
const updatedUser = await storage.update('test_users', createdUser.id, updates);
expect(updatedUser.name).toBe('Updated Name');
expect(updatedUser.id).toBe(createdUser.id);
});
it('should delete user', async () => {
const storage = sdk.storage();
const userData = TestDataFactory.user();
const createdUser = await storage.create('test_users', userData);
await storage.delete('test_users', createdUser.id);
const retrievedUser = await storage.get('test_users', createdUser.id);
expect(retrievedUser).toBeNull();
});
});// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:4173',
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run build && npm run preview',
port: 4173,
reuseExistingServer: !process.env.CI
}
});// e2e/user-management.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
// Login as admin
await page.goto('/login');
await page.fill('[data-testid="email"]', 'admin@example.com');
await page.fill('[data-testid="password"]', 'password');
await page.click('[data-testid="login-button"]');
// Wait for redirect to dashboard
await page.waitForURL('/dashboard');
});
test('should create new user', async ({ page }) => {
await page.goto('/users');
// Click create user button
await page.click('[data-testid="create-user-button"]');
// Fill form
await page.fill('[data-testid="user-email"]', 'newuser@example.com');
await page.fill('[data-testid="user-name"]', 'New User');
await page.selectOption('[data-testid="user-role"]', 'user');
// Submit form
await page.click('[data-testid="submit-button"]');
// Verify success message
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Verify user appears in list
await expect(page.locator('[data-testid="user-list"]')).toContainText('newuser@example.com');
});
test('should edit existing user', async ({ page }) => {
await page.goto('/users');
// Click edit button for first user
await page.click('[data-testid="edit-user-button"]');
// Update name
await page.fill('[data-testid="user-name"]', 'Updated Name');
// Submit form
await page.click('[data-testid="submit-button"]');
// Verify success message
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Verify updated name appears in list
await expect(page.locator('[data-testid="user-list"]')).toContainText('Updated Name');
});
test('should delete user', async ({ page }) => {
await page.goto('/users');
// Click delete button for first user
await page.click('[data-testid="delete-user-button"]');
// Confirm deletion
await page.click('[data-testid="confirm-delete"]');
// Verify success message
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Verify user is removed from list
await expect(page.locator('[data-testid="user-list"]')).not.toContainText('test@example.com');
});
});// e2e/api.spec.ts
import { test, expect } from '@playwright/test';
test.describe('API Endpoints', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
// Login to get auth token
const response = await request.post('/api/auth/login', {
data: {
email: 'admin@example.com',
password: 'password'
}
});
const data = await response.json();
authToken = data.token;
});
test('GET /api/users should return users list', async ({ request }) => {
const response = await request.get('/api/users', {
headers: {
Authorization: `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.users).toBeDefined();
expect(Array.isArray(data.users)).toBe(true);
});
test('POST /api/users should create new user', async ({ request }) => {
const userData = {
email: 'apitest@example.com',
name: 'API Test User',
role: 'user'
};
const response = await request.post('/api/users', {
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
data: userData
});
expect(response.status()).toBe(201);
const data = await response.json();
expect(data.user.email).toBe(userData.email);
expect(data.user.name).toBe(userData.name);
});
test('PUT /api/users/:id should update user', async ({ request }) => {
// First create a user
const createResponse = await request.post('/api/users', {
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
data: {
email: 'updatetest@example.com',
name: 'Update Test User',
role: 'user'
}
});
const createdUser = await createResponse.json();
// Then update the user
const updateResponse = await request.put(`/api/users/${createdUser.user.id}`, {
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
data: {
name: 'Updated Name'
}
});
expect(updateResponse.status()).toBe(200);
const updatedData = await updateResponse.json();
expect(updatedData.user.name).toBe('Updated Name');
});
});# artillery.yml
config:
target: 'http://localhost:5173'
phases:
- duration: 60
arrivalRate: 10
- duration: 120
arrivalRate: 20
- duration: 60
arrivalRate: 5
payload:
path: './test-data.csv'
fields:
- 'email'
- 'name'
scenarios:
- name: 'User CRUD Operations'
weight: 70
flow:
- post:
url: '/api/auth/login'
json:
email: 'admin@example.com'
password: 'password'
capture:
- json: '$.token'
as: 'authToken'
- get:
url: '/api/users'
headers:
Authorization: 'Bearer {{ authToken }}'
- post:
url: '/api/users'
headers:
Authorization: 'Bearer {{ authToken }}'
Content-Type: 'application/json'
json:
email: '{{ email }}'
name: '{{ name }}'
role: 'user'
- name: 'Dashboard Access'
weight: 30
flow:
- get:
url: '/dashboard'
- get:
url: '/api/stats'// src/test/performance/storage.perf.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
import { TestDataFactory } from '../utils/test-data';
describe('Storage Performance', () => {
let sdk: AsterismsBackendSDK;
beforeEach(async () => {
sdk = getAsterismsBackendSDK(testConfig);
await sdk.boot();
});
it('should handle bulk operations efficiently', async () => {
const storage = sdk.storage();
const users = Array.from({ length: 1000 }, () => TestDataFactory.user());
const startTime = performance.now();
// Create users in batches
const batchSize = 100;
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize);
await Promise.all(batch.map((user) => storage.create('perf_test_users', user)));
}
const endTime = performance.now();
const duration = endTime - startTime;
// Expect to process 1000 users in under 5 seconds
expect(duration).toBeLessThan(5000);
console.log(`Processed ${users.length} users in ${duration.toFixed(2)}ms`);
});
it('should handle concurrent reads efficiently', async () => {
const storage = sdk.storage();
// Create test data
const users = Array.from({ length: 100 }, () => TestDataFactory.user());
const createdUsers = await Promise.all(
users.map((user) => storage.create('perf_test_users', user))
);
const startTime = performance.now();
// Perform concurrent reads
const readPromises = createdUsers.map((user) => storage.get('perf_test_users', user.id));
const results = await Promise.all(readPromises);
const endTime = performance.now();
const duration = endTime - startTime;
// Expect all reads to complete
expect(results).toHaveLength(100);
expect(results.every((result) => result !== null)).toBe(true);
// Expect to read 100 users in under 1 second
expect(duration).toBeLessThan(1000);
console.log(`Read ${results.length} users in ${duration.toFixed(2)}ms`);
});
});# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Build application
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella{
"scripts": {
"test": "vitest",
"test:unit": "vitest run --reporter=verbose --coverage",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:performance": "vitest run --config vitest.performance.config.ts",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage"
}
}// ✅ Good: Organized test structure
describe('UserService', () => {
describe('CRUD Operations', () => {
describe('create', () => {
it('should create user with valid data', async () => {
// Test implementation
});
it('should reject invalid email', async () => {
// Test implementation
});
});
describe('findById', () => {
it('should return user when found', async () => {
// Test implementation
});
it('should return null when not found', async () => {
// Test implementation
});
});
});
});
// ❌ Bad: Flat test structure
describe('UserService', () => {
it('should create user', async () => {});
it('should find user', async () => {});
it('should update user', async () => {});
it('should delete user', async () => {});
});// ✅ Good: Use factories for consistent test data
const user = TestDataFactory.user({
email: 'test@example.com',
role: 'admin'
});
// ✅ Good: Clean up test data
afterEach(async () => {
await cleanupTestData();
});
// ❌ Bad: Hardcoded test data
const user = {
id: '123',
email: 'test@example.com',
name: 'Test User'
// ... lots of hardcoded fields
};// ✅ Good: Reset mocks between tests
beforeEach(() => {
vi.clearAllMocks();
});
// ✅ Good: Verify mock calls
expect(mockSDK.storage().create).toHaveBeenCalledWith(
'users',
expect.objectContaining({
email: 'test@example.com'
})
);
// ❌ Bad: Shared mock state between tests
let mockSDK = createMockSDK();
// This mock will retain state between tests// ✅ Good: Test error conditions
it('should handle network errors gracefully', async () => {
mockSDK.storage().get.mockRejectedValue(new Error('Network error'));
await expect(userService.findById('123')).rejects.toThrow('Network error');
});
// ✅ Good: Test specific error types
it('should handle validation errors', async () => {
const invalidUser = { email: 'invalid-email' };
await expect(userService.create(invalidUser)).rejects.toThrow('Invalid email format');
});// src/test/debug.ts
import { beforeEach } from 'vitest';
beforeEach(() => {
// Enable debug logging in tests
if (process.env.NODE_ENV === 'test' && process.env.DEBUG) {
console.log('=== Test Debug Mode ===');
}
});
// Helper for debugging test state
export function debugTestState(label: string, data: any) {
if (process.env.DEBUG) {
console.log(`[DEBUG] ${label}:`, JSON.stringify(data, null, 2));
}
}# Run tests with debug output
DEBUG=true npm run test
# Run specific test file
npm run test -- UserService.test.ts
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run E2E tests in headed mode
npm run test:e2e -- --headed
# Run E2E tests in debug mode
npm run test:e2e -- --debug