Testing patterns for Rust and TypeScript, with focus on Tauri-specific mocking.
npm run check:all # All tests and checks
npm run test # TypeScript tests (watch mode)
npm run test:run # TypeScript tests (single run)
npm run rust:test # Rust testsUses Vitest + @testing-library/react. Configuration in vitest.config.ts.
Place tests next to the code they test:
src/components/ui/Button.tsx
src/components/ui/Button.test.tsx
Tauri commands must be mocked since tests run outside the Tauri environment. Mocks are configured in src/test/setup.ts:
// src/test/setup.ts
import { vi } from 'vitest'
// Mock Tauri event APIs
vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn().mockResolvedValue(() => {}),
}))
vi.mock('@tauri-apps/plugin-updater', () => ({
check: vi.fn().mockResolvedValue(null),
}))
// Mock typed Tauri bindings (tauri-specta generated)
vi.mock('@/lib/tauri-bindings', () => ({
commands: {
greet: vi.fn().mockResolvedValue('Hello, test!'),
loadPreferences: vi
.fn()
.mockResolvedValue({ status: 'ok', data: { theme: 'system' } }),
savePreferences: vi.fn().mockResolvedValue({ status: 'ok', data: null }),
sendNativeNotification: vi
.fn()
.mockResolvedValue({ status: 'ok', data: null }),
saveEmergencyData: vi.fn().mockResolvedValue({ status: 'ok', data: null }),
loadEmergencyData: vi.fn().mockResolvedValue({ status: 'ok', data: null }),
cleanupOldRecoveryFiles: vi
.fn()
.mockResolvedValue({ status: 'ok', data: 0 }),
},
}))import { vi } from 'vitest'
import { commands } from '@/lib/tauri-bindings'
const mockCommands = vi.mocked(commands)
test('loads preferences', async () => {
mockCommands.loadPreferences.mockResolvedValue({
status: 'ok',
data: { theme: 'dark' },
})
// Test code that calls loadPreferences
})Components using TanStack Query need a provider wrapper:
// src/test/utils.ts
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactNode } from 'react'
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
}
export function TestProviders({ children }: { children: ReactNode }) {
const queryClient = createTestQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}Usage:
import { render } from '@testing-library/react'
import { TestProviders } from '@/test/utils'
test('component with query', () => {
render(
<TestProviders>
<MyComponent />
</TestProviders>
)
})import { renderHook, act } from '@testing-library/react'
import { useUIStore } from '@/store/ui-store'
test('toggles sidebar visibility', () => {
const { result } = renderHook(() => useUIStore())
expect(result.current.leftSidebarVisible).toBe(true)
act(() => {
result.current.setLeftSidebarVisible(false)
})
expect(result.current.leftSidebarVisible).toBe(false)
})#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preferences_default() {
let prefs = AppPreferences::default();
assert_eq!(prefs.theme, "system");
}
}#[tokio::test]
async fn test_async_operation() {
let result = some_async_fn().await;
assert!(result.is_ok());
}Use tempfile for tests that need file system access:
use tempfile::TempDir;
#[test]
fn test_file_operations() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.json");
// Test write
std::fs::write(&file_path, "{}").unwrap();
// Test read
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "{}");
}When adding new Tauri commands, update src/test/setup.ts:
vi.mock('@/lib/tauri-bindings', () => ({
commands: {
// ... existing mocks
myNewCommand: vi.fn().mockResolvedValue({ status: 'ok', data: null }),
},
}))| Do | Don't |
|---|---|
| Mock Tauri commands in setup.ts | Call real Tauri APIs in tests |
Use vi.mocked() for type-safe mocks |
Use untyped mock assertions |
| Test user-visible behavior | Test implementation details |
Use tempfile for Rust file tests |
Write to real file system |