Testing
Testing Guide
Section titled “Testing Guide”This guide covers how to write and run tests for Feima Copilot.
Testing Framework
Section titled “Testing Framework”Feima Copilot uses:
- Jest - Test framework
- @vscode/test-electron - VS Code testing utilities
- ts-jest - TypeScript support
Running Tests
Section titled “Running Tests”Run All Tests
Section titled “Run All Tests”npm testRun Tests with Coverage
Section titled “Run Tests with Coverage”npm run test:coverageWatch Mode
Section titled “Watch Mode”npm run test:watchRun Specific Test File
Section titled “Run Specific Test File”npm test -- src/auth/__tests__/oauthService.test.tsRun Tests Matching Pattern
Section titled “Run Tests Matching Pattern”npm test -- --testNamePattern="OAuth"Test Structure
Section titled “Test Structure”Tests are located in src/**/__tests__/ directories:
src/├── auth/│ ├── oauthService.ts│ └── __tests__/│ ├── oauthService.test.ts│ └── sessionManager.test.ts├── models/│ ├── languageModelProvider.ts│ └── __tests__/│ └── languageModelProvider.test.ts└── services/ ├── feimaApiClient.ts └── __tests__/ └── feimaApiClient.test.tsWriting Tests
Section titled “Writing Tests”Basic Unit Test
Section titled “Basic Unit Test”import { SessionManager } from '../sessionManager';
describe('SessionManager', () => { let sessionManager: SessionManager;
beforeEach(() => { sessionManager = new SessionManager(); });
describe('getSession', () => { it('should return null when no session exists', () => { expect(sessionManager.getSession()).toBeNull(); });
it('should return the current session when exists', () => { const mockSession = { accessToken: 'test-token', refreshToken: 'refresh-token', expiresAt: new Date(), user: { email: 'test@example.com', sub: '123', provider: 'wechat' } };
sessionManager.setSession(mockSession); expect(sessionManager.getSession()).toEqual(mockSession); }); });});Testing Async Code
Section titled “Testing Async Code”describe('OAuth2Service', () => { it('should sign in successfully', async () => { const service = new OAuth2Service(mockContext); const session = await service.signIn();
expect(session).toBeDefined(); expect(session.accessToken).toBeDefined(); expect(session.user.email).toBeDefined(); });
it('should throw error on authentication failure', async () => { const service = new OAuth2Service(mockContext); // Mock API to return error mockApi.rejects(new Error('Authentication failed'));
await expect(service.signIn()).rejects.toThrow('Authentication failed'); });});Mocking Dependencies
Section titled “Mocking Dependencies”import { FeimaApiClient } from '../../services/feimaApiClient';
jest.mock('../../services/feimaApiClient');
describe('LanguageModelProvider', () => { let mockApiClient: jest.Mocked<FeimaApiClient>; let provider: LanguageModelProvider;
beforeEach(() => { mockApiClient = { models: jest.fn(), completions: jest.fn(), chat: jest.fn() } as any;
provider = new LanguageModelProvider(mockContext, mockApiClient); });
it('should fetch models from API', async () => { const mockModels = [ { id: 'deepseek-coder-v2', name: 'DeepSeek Coder V2' } ]; mockApiClient.models.mockResolvedValue(mockModels);
await provider.refreshModels(); expect(mockApiClient.models).toHaveBeenCalled(); });});Testing VS Code Extensions
Section titled “Testing VS Code Extensions”import * as vscode from 'vscode';import { activate } from '../extension';
describe('Extension', () => { let mockContext: vscode.ExtensionContext;
beforeEach(() => { mockContext = { subscriptions: [], secrets: {} as any } as any; });
it('should register commands', () => { activate(mockContext);
expect(mockContext.subscriptions.length).toBeGreaterThan(0); });
it('should register commands with correct IDs', () => { const registerCommandSpy = jest.spyOn(vscode.commands, 'registerCommand'); activate(mockContext);
expect(registerCommandSpy).toHaveBeenCalledWith('feima.signIn', expect.any(Function)); expect(registerCommandSpy).toHaveBeenCalledWith('feima.signOut', expect.any(Function)); });});Integration Tests
Section titled “Integration Tests”Testing OAuth Flow
Section titled “Testing OAuth Flow”describe('OAuth Integration Tests', () => { it('should complete full OAuth flow', async () => { const service = new OAuth2Service(context);
// Start sign in const authUrl = await service.getAuthorizationUrl(); expect(authUrl).toContain('idp.feimacode.cn');
// Simulate callback const code = 'mock-authorization-code'; const session = await service.handleCallback(code);
expect(session).toBeDefined(); expect(session.accessToken).toBeTruthy(); });});Testing API Communication
Section titled “Testing API Communication”describe('API Integration Tests', () => { it('should make successful API request', async () => { const api = new FeimaApiClient(authService);
const response = await api.chat({ model: 'deepseek-coder-v2', messages: [{ role: 'user', content: 'Hello' }] });
expect(response.choices).toBeDefined(); expect(response.choices[0].message).toBeDefined(); });});Testing Authentication
Section titled “Testing Authentication”Testing Token Refresh
Section titled “Testing Token Refresh”describe('Token Refresh', () => { it('should refresh token when expired', async () => { const expiredSession = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token', expiresAt: new Date(Date.now() - 1000), // Expired user: { email: 'test@example.com', sub: '123', provider: 'wechat' } };
const service = new OAuth2Service(context); service.setSession(expiredSession);
await service.refreshAccessToken();
const newSession = service.getSession(); expect(newSession?.accessToken).not.toBe('expired-token'); });});Testing Session Persistence
Section titled “Testing Session Persistence”describe('Session Persistence', () => { it('should persist session to secrets', async () => { const mockSecrets = { get: jest.fn(), store: jest.fn() };
const service = new OAuth2Service(context);
const session = await service.signIn(); expect(mockSecrets.store).toHaveBeenCalledWith( 'session', expect.any(String) ); });});Testing Models
Section titled “Testing Models”Testing Model Selection
Section titled “Testing Model Selection”describe('Model Selection', () => { it('should return correct model by ID', () => { const provider = new LanguageModelProvider(context);
const model = provider.getModel('deepseek-coder-v2'); expect(model?.id).toBe('deepseek-coder-v2'); expect(model?.name).toBe('DeepSeek Coder V2'); });
it('should return undefined for unknown model', () => { const provider = new LanguageModelProvider(context);
const model = provider.getModel('unknown-model'); expect(model).toBeUndefined(); });});Test Configuration
Section titled “Test Configuration”Jest Config
Section titled “Jest Config”The Jest configuration is in jest.config.js:
module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.test.ts'], collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**' ], coverageThreshold: { global: { branches: 70, functions: 70, lines: 70, statements: 70 } }};Best Practices
Section titled “Best Practices”1. Test Behavior, Not Implementation
Section titled “1. Test Behavior, Not Implementation”// Good - tests behaviorit('should sign in user', async () => { const session = await service.signIn(); expect(session).toBeDefined(); expect(session.user.email).toBeTruthy();});
// Bad - tests implementationit('should call API with correct endpoint', async () => { await service.signIn(); expect(mockApi.calledWith).toBe('https://idp.feimacode.cn');});2. Use Descriptive Test Names
Section titled “2. Use Descriptive Test Names”// Goodit('should throw error when authentication fails due to invalid credentials');
// Badit('should handle error');3. Isolate Tests
Section titled “3. Isolate Tests”describe('Feature', () => { beforeEach(() => { // Reset state before each test mockApi.resetMock(); });
afterEach(() => { // Clean up after each test jest.clearAllMocks(); });});4. Use Test Doubles
Section titled “4. Use Test Doubles”// Mock external dependenciesconst mockApiClient = { chat: jest.fn()} as jest.Mocked<FeimaApiClient>;
// Stub specific methodsconst stubService = { signIn: jest.fn().mockResolvedValue(mockSession)};Continuous Integration
Section titled “Continuous Integration”Tests run automatically on:
- Pull requests
- Push to main branch
- Nightly builds
GitHub Actions
Section titled “GitHub Actions”- name: Run tests run: npm test
- name: Check coverage run: npm run test:coverage
- name: Upload coverage uses: codecov/codecov-action@v3Debugging Tests
Section titled “Debugging Tests”Running Tests in VS Code
Section titled “Running Tests in VS Code”- Install the Jest Runner extension
- Click the play button next to tests
- Set breakpoints in test files
Debugging with VS Code
Section titled “Debugging with VS Code”Add to .vscode/launch.json:
{ "type": "node", "request": "launch", "name": "Jest Current File", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen"}Common Issues
Section titled “Common Issues”Timeout Errors
Section titled “Timeout Errors”// Increase timeout for slow testsit('should complete slow operation', async () => { // test code}, 30000); // 30 second timeoutAsync Test Issues
Section titled “Async Test Issues”// Make sure to use async/awaitit('should handle async operation', async () => { const result = await asyncFunction(); expect(result).toBeTruthy();});Mock Issues
Section titled “Mock Issues”// Clear mocks between testsafterEach(() => { jest.clearAllMocks();});
// Or reset all mocksafterEach(() => { jest.resetAllMocks();});Resources
Section titled “Resources”Next Steps
Section titled “Next Steps”- Development Setup - Set up development environment
- Building Guide - Build and package
- API Reference - Extension API