microsoft / microsoft/vscode-python-environments
Improve testability of environment managers by creating a mock/fake NativePythonFinder
@karthiknadig đang làm issue này rồi.
Từ ngày 3/2/2026.
- Ngôn ngữ chính
- TypeScript
- Star
- 138
- Fork
- 62
- Merge trung bình
- 1 ngày 4 giờ
- Pull request đã merge (30 ngày)
- 35
Mô tả
# 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:
1. Spawns an external native binary (`pet.exe`/`pet`) via JSON-RPC
2. Requires real file system operations
3. 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**: `NativePythonFinder` is only partially mocked in `interpreterSelection.unit.test.ts` for the `resolve()` method
## Proposed Solution
### 1. Create a Mock/Fake NativePythonFinder
Create a reusable `MockNativePythonFinder` class in `src/test/mocks/mockNativePythonFinder.ts`:
```typescript
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;
}
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 => {
// 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 => {
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:
```typescript
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`:
```typescript
export function createMockPythonEnvironmentApi(): Partial {
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:
```typescript
// 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
```typescript
// 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.ts` with full interface implementation
- [ ] Create `src/test/fixtures/nativeFinderData.ts` with realistic test data
- [ ] Create `src/test/mocks/mockPythonApi.ts` for 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
1. **Abstract child process spawning**: The `*Utils.ts` files (e.g., `condaUtils.ts`) spawn child processes. These could be abstracted through wrapper functions in `common/childProcess.apis.ts` for easier mocking.
2. **File system operations**: Use the existing `workspace.fs.apis` wrappers consistently across managers for easier stubbing.
3. **Settings access**: Ensure all settings are accessed through `workspace.apis` wrappers rather than direct `vscode.workspace.getConfiguration()` calls.
## Related Files
- `src/managers/common/nativePythonFinder.ts` - The interface to mock
- `src/test/mocks/helper.ts` - Existing mock helpers
- `src/test/features/interpreterSelection.unit.test.ts` - Example of partial NativePythonFinder mocking
- `src/managers/*/` - All manager implementations needing tests
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Đánh giá
Issue này chưa được đánh giá.