microsoft / microsoft/vscode-python-environments
Improve testability of environment managers by creating a mock/fake NativePythonFinder
@karthiknadig 已经在做这个了。
开始于 2026年2月3日。
- 主要语言
- TypeScript
- 星标
- 138
- 派生
- 62
- 平均合并
- 1 天 4 小时
- 30 天内合并 PR
- 35
描述
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, poetry
3. 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
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
评估
这个 Issue 还没有评估数据。