This document provides comprehensive guidance on testing practices for the Stashpad project.
- Running Tests
- Frontend Testing (TypeScript/Svelte)
- Backend Testing (Rust)
- Writing New Tests
- Code Coverage
- CI/CD Integration
- Best Practices
# Run all tests once
npm run test
# Run tests in watch mode (useful during development)
npm run test:watch
# Run tests with UI (visual interface)
npm run test:ui
# Run tests with coverage report
npm run test:coverage# Run all Rust tests
cd src-tauri
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_name
# Run tests in release mode (faster)
cargo test --release- Framework: Vitest v4.0+
- Testing Library: @testing-library/svelte
- Environment: jsdom (for DOM testing)
- Mocking: Vitest's built-in mocking
All frontend tests are located in __tests__ directories next to the code they test:
src/lib/
├── utils/
│ ├── __tests__/
│ │ ├── markdown.test.ts
│ │ ├── format.test.ts
│ │ ├── date.test.ts
│ │ └── version.test.ts
│ ├── markdown.ts
│ ├── format.ts
│ └── ...
├── services/
│ ├── __tests__/
│ │ └── desktop-adapter.test.ts
│ └──desktop-adapter.ts
└── components/
├── __tests__/
│ └── (component tests would go here)
└── ...
import { describe, it, expect } from 'vitest';
import { formatBytes } from '../format';
describe('formatBytes', () => {
it('should format bytes correctly', () => {
expect(formatBytes(1024)).toBe('1 KB');
expect(formatBytes(1024 * 1024)).toBe('1 MB');
});
});import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DesktopStorageAdapter } from '../desktop-adapter';
// Mock the Tauri API
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
import { invoke } from '@tauri-apps/api/core';
describe('DesktopStorageAdapter', () => {
let adapter: DesktopStorageAdapter;
let mockInvoke: ReturnType<typeof vi.fn>;
beforeEach(() => {
adapter = new DesktopStorageAdapter();
mockInvoke = invoke as ReturnType<typeof vi.fn>;
mockInvoke.mockClear();
});
it('should call load_stashes command', async () => {
mockInvoke.mockResolvedValue([]);
await adapter.loadStashes();
expect(mockInvoke).toHaveBeenCalledWith('load_stashes');
});
});All tests have access to mocked Tauri APIs via vitest.setup.ts:
@tauri-apps/api/core- Mockedinvokefunction@tauri-apps/api/event- Mocked event listeners
Rust tests should cover:
- Unit Tests: Individual functions and methods
- Integration Tests: Complete workflows with in-memory databases
- Edge Cases: Error handling, boundary conditions
Add tests at the end of src-tauri/src/lib.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_app_dir() {
let dir = get_app_dir();
assert!(dir.ends_with(".stashpad"));
}
#[test]
fn test_validate_settings() {
let mut settings = Settings::default();
settings.new_stash_position = "invalid".to_string();
let validated = validate_settings(settings);
assert_eq!(validated.new_stash_position, "top");
}
}Add tests in src-tauri/src/db.rs:
#[cfg(test)]
mod tests {
use super::*;
fn create_test_db() -> DbManager {
DbManager::new_in_memory().expect("Failed to create test DB")
}
#[test]
fn test_save_and_get_stash() {
let mut db = create_test_db();
let stash = StashItem {
id: "test-123".to_string(),
content: "test content".to_string(),
attachments: vec![],
files: vec![],
created_at: crate::time::now_iso(),
context_id: "default".to_string(),
completed: false,
completed_at: None,
};
db.save_stash(&stash, None).expect("Failed to save stash");
let stashes = db.get_stashes().expect("Failed to get stashes");
assert_eq!(stashes.len(), 1);
assert_eq!(stashes[0].id, "test-123");
}
}-
Name Tests Descriptively: Test names should clearly describe what they're testing
- Good:
test_save_asset_with_valid_file - Bad:
test1
- Good:
-
Follow AAA Pattern: Arrange, Act, Assert
it('should format bytes correctly', () => { // Arrange const bytes = 1024; // Act const result = formatBytes(bytes); // Assert expect(result).toBe('1 KB'); });
-
Test One Thing: Each test should verify one specific behavior
-
Use Descriptive Assertions: Make failures easy to understand
// Good expect(result).toBe('1 KB'); // Better with message expect(result).toBe('1 KB'); // If using jest-dom matchers
-
Mock External Dependencies: Don't make real API calls or file system operations in tests
- New Features: Add tests before or alongside new code (TDD encouraged)
- Bug Fixes: Add a regression test that reproduces the bug, then fix it
- Refactoring: Ensure existing tests pass after refactoring
npm run test:coverageThis generates a coverage report in:
- Terminal output (text summary)
coverage/index.html(interactive HTML report)
- Utilities: Aim for >90% coverage
- Services: Aim for >80% coverage
- Components: Aim for >70% coverage
- Overall: Maintain >75% coverage
Files automatically excluded (see vitest.config.ts):
- Test files (
*.test.ts,*.spec.ts) - Mock files (
__mocks__/**) - Configuration files (
*.config.ts)
Tests run automatically on:
- Every push to
mainbranch - Every pull request
- Manual workflow dispatch
Located at .github/workflows/test.yml (to be created):
name: Tests
on:
push:
branches: [main]
pull_request:
jobs:
frontend-tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm run test:coverage
backend-tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
- run: cd src-tauri && cargo test✅ Write tests for all new features
✅ Keep tests simple and focused
✅ Use meaningful test descriptions
✅ Mock external dependencies
✅ Test edge cases and error conditions
✅ Run tests before committing
✅ Keep test code clean and maintainable
❌ Skip tests because "it's simple code"
❌ Write tests that depend on execution order
❌ Make tests that require manual setup
❌ Test implementation details (test behavior, not internals)
❌ Leave failing tests in the codebase
❌ Copy-paste test code without understanding it
-
Locale-Dependent Tests: Tests that rely on dates or localization should handle multiple locales
// Bad: Will fail in non-English locales expect(formattedDate).toBe('Dec 2025'); // Good: Flexible assertion expect(formattedDate).toContain('2025');
Fix "now" with
setClockfrom$lib/utils/time, and restore it withsetClock(null)inafterEach. All app code reads the clock through that module, neverDate-npm run check:temporalfails on aDateanywhere, tests included. Build timestamps withfromEpochMsortoCanonicalso they have the same nine-digit form the app writes.vi.useFakeTimers()still moves the clock as well, because the polyfill reads the time through it. -
File API Mocking: jsdom doesn't fully support File APIs
// Mock arrayBuffer for File objects in tests mockFile.arrayBuffer = vi.fn().mockResolvedValue(buffer);
-
Async Testing: Always await async operations
// Bad it('should load data', () => { fetchData(); // Not awaited! expect(data).toBeDefined(); // Will fail }); // Good it('should load data', async () => { await fetchData(); expect(data).toBeDefined(); });
If you encounter issues with tests:
- Check this guide and the links above
- Look at existing tests for patterns
- Run tests with
--helpto see all options - Ask the team for help!
Remember: Good tests are documentation that never lies. Write tests that make your code easier to understand and maintain.