Testing Guide

Testing strategies, patterns, and best practices for DesQTA

Testing Overview

DesQTA uses a comprehensive testing strategy covering unit tests, integration tests, and end-to-end tests. This guide covers testing patterns and best practices.

Testing Stack

Tools

  • Vitest: Unit and integration testing
  • @testing-library/svelte: Component testing
  • Playwright: E2E testing (planned)
  • Mock Service Worker: API mocking

Unit Testing

Component Testing

Test Svelte components with @testing-library/svelte:

// Button.test.ts
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Button from './Button.svelte';

test('renders button with text', () => {
  render(Button, { props: { children: 'Click me' } });
  expect(screen.getByText('Click me')).toBeInTheDocument();
});

test('calls onclick when clicked', async () => {
  const handleClick = vi.fn();
  render(Button, { props: { onclick: handleClick } });
  
  const button = screen.getByRole('button');
  await fireEvent.click(button);
  
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Service Testing

Test services in isolation:

// authService.test.ts
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { authService } from './authService';
import { invoke } from '@tauri-apps/api/core';

vi.mock('@tauri-apps/api/core');

describe('authService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  test('checkSession returns true when session exists', async () => {
    vi.mocked(invoke).mockResolvedValue(true);
    
    const result = await authService.checkSession();
    
    expect(result).toBe(true);
    expect(invoke).toHaveBeenCalledWith('check_session_exists');
  });

  test('checkSession throws error on failure', async () => {
    vi.mocked(invoke).mockRejectedValue(new Error('Session check failed'));
    
    await expect(authService.checkSession()).rejects.toThrow();
  });
});

Store Testing

Test Svelte stores:

// theme.test.ts
import { describe, test, expect, beforeEach } from 'vitest';
import { get } from 'svelte/store';
import { theme, updateTheme } from './theme';

describe('theme store', () => {
  beforeEach(() => {
    theme.set('system');
  });

  test('updates theme', async () => {
    await updateTheme('dark');
    expect(get(theme)).toBe('dark');
  });

  test('applies theme to DOM', async () => {
    await updateTheme('dark');
    expect(document.documentElement.classList.contains('dark')).toBe(true);
  });
});

Integration Testing

Component with Service

Test components that use services:

// AssessmentList.test.ts
import { render, screen, waitFor } from '@testing-library/svelte';
import { vi } from 'vitest';
import AssessmentList from './AssessmentList.svelte';
import { assessmentService } from '$lib/services/assessmentService';

vi.mock('$lib/services/assessmentService');

describe('AssessmentList', () => {
  test('loads and displays assessments', async () => {
    const mockAssessments = [
      { id: 1, title: 'Math Test', due: '2024-12-20' },
      { id: 2, title: 'English Essay', due: '2024-12-25' }
    ];

    vi.mocked(assessmentService.loadAssessments).mockResolvedValue(mockAssessments);

    render(AssessmentList);

    await waitFor(() => {
      expect(screen.getByText('Math Test')).toBeInTheDocument();
      expect(screen.getByText('English Essay')).toBeInTheDocument();
    });
  });
});

Mocking

Mock Tauri Commands

import { vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';

vi.mock('@tauri-apps/api/core', () => ({
  invoke: vi.fn()
}));

// In tests
vi.mocked(invoke).mockResolvedValue(mockData);

Mock Services

import { vi } from 'vitest';
import { authService } from '$lib/services/authService';

vi.mock('$lib/services/authService', () => ({
  authService: {
    checkSession: vi.fn(),
    loadUserInfo: vi.fn()
  }
}));

Mock Stores

import { vi } from 'vitest';
import * as themeStore from '$lib/stores/theme';

vi.mock('$lib/stores/theme', () => ({
  theme: {
    subscribe: vi.fn((callback) => {
      callback('dark');
      return () => {};
    })
  }
}));

Testing Patterns

Testing Async Operations

test('handles async data loading', async () => {
  const { component } = render(DataLoader);
  
  // Wait for loading to complete
  await waitFor(() => {
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });
  
  // Assert data is displayed
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

Testing Error States

test('displays error message on failure', async () => {
  vi.mocked(fetchData).mockRejectedValue(new Error('Failed'));
  
  render(DataComponent);
  
  await waitFor(() => {
    expect(screen.getByText('Failed to load data')).toBeInTheDocument();
  });
});

Testing User Interactions

test('handles user input', async () => {
  const { component } = render(InputComponent);
  
  const input = screen.getByRole('textbox');
  await fireEvent.input(input, { target: { value: 'test' } });
  
  expect(input).toHaveValue('test');
});

Testing Form Submissions

test('submits form with data', async () => {
  const handleSubmit = vi.fn();
  render(FormComponent, { props: { onSubmit: handleSubmit } });
  
  const input = screen.getByLabelText('Name');
  await fireEvent.input(input, { target: { value: 'John' } });
  
  const submitButton = screen.getByRole('button', { name: 'Submit' });
  await fireEvent.click(submitButton);
  
  expect(handleSubmit).toHaveBeenCalledWith({ name: 'John' });
});

E2E Testing (Planned)

Playwright Setup

// e2e/assessments.spec.ts
import { test, expect } from '@playwright/test';

test('loads assessments page', async ({ page }) => {
  await page.goto('/assessments');
  await expect(page.locator('h1')).toContainText('Assessments');
});

test('navigates to assessment detail', async ({ page }) => {
  await page.goto('/assessments');
  await page.click('[data-testid="assessment-card"]');
  await expect(page).toHaveURL(/\/assessments\/\d+/);
});

Test Utilities

Test Helpers

// test-utils.ts
import { render } from '@testing-library/svelte';
import { vi } from 'vitest';

export function renderWithProviders(component: any, props = {}) {
  // Setup providers, mocks, etc.
  return render(component, { props });
}

export function createMockUser() {
  return {
    id: 1,
    email: 'test@example.com',
    userName: 'testuser'
  };
}

Custom Matchers

// test-setup.ts
import { expect } from 'vitest';

expect.extend({
  toBeValidAssessment(received) {
    const pass = 
      received.id &&
      received.title &&
      received.due;
    
    return {
      pass,
      message: () => `Expected ${received} to be a valid assessment`
    };
  }
});

Coverage

Coverage Goals

  • Components: 80%+ coverage
  • Services: 90%+ coverage
  • Utilities: 90%+ coverage
  • Stores: 85%+ coverage

Running Coverage

# Run tests with coverage
npm run test:coverage

# View coverage report
open coverage/index.html

Best Practices

1. Test Behavior, Not Implementation

// ✅ Good - Test behavior
test('displays error when fetch fails', async () => {
  // Test what user sees
});

// ❌ Avoid - Test implementation
test('calls fetchData function', () => {
  // Don't test internal implementation
});

2. Use Descriptive Test Names

// ✅ Good - Descriptive
test('displays loading spinner while fetching assessments', () => {});

// ❌ Avoid - Vague
test('loading', () => {});

3. Arrange-Act-Assert Pattern

test('example', () => {
  // Arrange - Setup
  const data = createMockData();
  
  // Act - Execute
  const result = processData(data);
  
  // Assert - Verify
  expect(result).toBe(expected);
});

4. Keep Tests Isolated

// ✅ Good - Isolated
test('test 1', () => {
  // Doesn't depend on other tests
});

// ❌ Avoid - Dependent
let sharedState;
test('test 1', () => {
  sharedState = 'value';
});
test('test 2', () => {
  expect(sharedState).toBe('value'); // Depends on test 1
});

5. Mock External Dependencies

// ✅ Good - Mock external calls
vi.mock('@tauri-apps/api/core');

// ❌ Avoid - Real API calls in tests
// Don't make actual API calls

Running Tests

Development

# Watch mode
npm run test:watch

# Run specific test file
npm run test Button.test.ts

# Run tests matching pattern
npm run test -- --grep "button"

CI/CD

# Run all tests
npm run test

# Run with coverage
npm run test:coverage

Next Steps