-
Notifications
You must be signed in to change notification settings - Fork 37
Description
Problem
The environment managers (Conda, Pyenv, Pipenv, Poetry, Venv, SysPython) lack comprehensive unit tests because testing them is complex. The main barrier is the NativePythonFinder dependency which:
- Spawns an external native binary (
pet.exe/pet) via JSON-RPC - Requires real file system operations
- Depends on actual Python installations being present
Currently, only helper utilities within managers have tests (e.g., pipUtils, venvUtils, installArgs), but the manager classes themselves (CondaEnvManager, VenvManager, SysPythonManager, PyenvManager, PipenvManager, PoetryManager) are untested.
Current State
- Tested: Helper functions in
src/test/managers/builtin/(pipUtils, venvUtils, etc.) - Untested: Core manager classes in
src/managers/*/ - Limited mocking:
NativePythonFinderis only partially mocked ininterpreterSelection.unit.test.tsfor theresolve()method
Proposed Solution
1. Create a Mock/Fake NativePythonFinder
Create a reusable MockNativePythonFinder class in src/test/mocks/mockNativePythonFinder.ts:
import * as sinon from 'sinon';
import { NativePythonFinder, NativeInfo, NativeEnvInfo, NativePythonEnvironmentKind } from '../../managers/common/nativePythonFinder';
import { Uri } from 'vscode';
export interface MockNativeFinderOptions {
environments?: NativeEnvInfo[];
managers?: { tool: string; executable: string; version?: string }[];
resolveResults?: Map<string, NativeEnvInfo>;
}
export function createMockNativePythonFinder(options: MockNativeFinderOptions = {}): NativePythonFinder {
const { environments = [], managers = [], resolveResults = new Map() } = options;
return {
refresh: sinon.stub().callsFake(async (_hardRefresh: boolean, filterOptions?: NativePythonEnvironmentKind | Uri[]): Promise<NativeInfo[]> => {
// Filter by kind if specified
if (typeof filterOptions === 'string') {
return environments.filter(e => e.kind === filterOptions);
}
return [...environments, ...managers];
}),
resolve: sinon.stub().callsFake(async (executable: string): Promise<NativeEnvInfo> => {
const result = resolveResults.get(executable);
if (!result) {
throw new Error(`Unknown executable: ${executable}`);
}
return result;
}),
dispose: sinon.stub(),
};
}2. Create Test Fixture Data
Add src/test/fixtures/nativeFinderData.ts with predefined environment data:
import { NativeEnvInfo, NativePythonEnvironmentKind } from '../../managers/common/nativePythonFinder';
export const MOCK_CONDA_ENVS: NativeEnvInfo[] = [
{
displayName: 'base',
name: 'base',
executable: '/opt/conda/bin/python',
kind: NativePythonEnvironmentKind.conda,
version: '3.11.0',
prefix: '/opt/conda',
manager: { tool: 'conda', executable: '/opt/conda/bin/conda', version: '23.5.0' },
},
{
displayName: 'myenv',
name: 'myenv',
executable: '/opt/conda/envs/myenv/bin/python',
kind: NativePythonEnvironmentKind.conda,
version: '3.10.0',
prefix: '/opt/conda/envs/myenv',
manager: { tool: 'conda', executable: '/opt/conda/bin/conda', version: '23.5.0' },
},
];
export const MOCK_VENV_ENVS: NativeEnvInfo[] = [
{
displayName: '.venv',
name: '.venv',
executable: '/workspace/.venv/bin/python',
kind: NativePythonEnvironmentKind.venv,
version: '3.12.0',
prefix: '/workspace/.venv',
},
];
// ... similar for pyenv, pipenv, poetry3. Create Mock PythonEnvironmentApi
The managers also depend on PythonEnvironmentApi. Add src/test/mocks/mockPythonApi.ts:
export function createMockPythonEnvironmentApi(): Partial<PythonEnvironmentApi> {
return {
getEnvironmentManager: sinon.stub(),
getPackageManager: sinon.stub(),
registerEnvironmentManager: sinon.stub().returns({ dispose: sinon.stub() }),
registerPackageManager: sinon.stub().returns({ dispose: sinon.stub() }),
// ... other minimal stubs
};
}4. Simplify Manager Construction
Consider adding a factory or builder pattern for managers to make dependency injection clearer:
// In the manager files, add a factory function for testing
export function createCondaEnvManager(
nativeFinder: NativePythonFinder,
api: PythonEnvironmentApi,
log: LogOutputChannel,
): CondaEnvManager {
return new CondaEnvManager(nativeFinder, api, log);
}5. Example Test Structure
// src/test/managers/conda/condaEnvManager.unit.test.ts
import assert from 'node:assert';
import * as sinon from 'sinon';
import { createMockNativePythonFinder } from '../../mocks/mockNativePythonFinder';
import { createMockPythonEnvironmentApi } from '../../mocks/mockPythonApi';
import { createMockLogOutputChannel } from '../../mocks/helper';
import { MOCK_CONDA_ENVS } from '../../fixtures/nativeFinderData';
import { CondaEnvManager } from '../../../managers/conda/condaEnvManager';
suite('CondaEnvManager', () => {
let manager: CondaEnvManager;
let mockFinder: NativePythonFinder;
setup(() => {
mockFinder = createMockNativePythonFinder({
environments: MOCK_CONDA_ENVS,
});
const mockApi = createMockPythonEnvironmentApi();
const mockLog = createMockLogOutputChannel();
manager = new CondaEnvManager(mockFinder, mockApi as PythonEnvironmentApi, mockLog);
});
teardown(() => {
sinon.restore();
manager.dispose();
});
test('getEnvironments returns all conda environments', async () => {
await manager.initialize();
const envs = await manager.getEnvironments('all');
assert.strictEqual(envs.length, 2);
// ... assertions
});
});Acceptance Criteria
- Create
src/test/mocks/mockNativePythonFinder.tswith full interface implementation - Create
src/test/fixtures/nativeFinderData.tswith realistic test data - Create
src/test/mocks/mockPythonApi.tsfor API dependency - Add at least basic unit tests for each manager:
-
CondaEnvManager -
VenvManager -
SysPythonManager -
PyenvManager -
PipenvManager -
PoetryManager
-
- Document the testing patterns in the testing workflow instructions
Additional Simplifications
-
Abstract child process spawning: The
*Utils.tsfiles (e.g.,condaUtils.ts) spawn child processes. These could be abstracted through wrapper functions incommon/childProcess.apis.tsfor easier mocking. -
File system operations: Use the existing
workspace.fs.apiswrappers consistently across managers for easier stubbing. -
Settings access: Ensure all settings are accessed through
workspace.apiswrappers rather than directvscode.workspace.getConfiguration()calls.
Related Files
src/managers/common/nativePythonFinder.ts- The interface to mocksrc/test/mocks/helper.ts- Existing mock helperssrc/test/features/interpreterSelection.unit.test.ts- Example of partial NativePythonFinder mockingsrc/managers/*/- All manager implementations needing tests