microsoft / microsoft/vscode-python-environments

Improve testability of environment managers by creating a mock/fake NativePythonFinder

Open
#1,159 1 comment 0 reactions 1 assignee View on GitHub

@karthiknadig is already working on this.

Since Feb 3, 2026.

enhancement testing
Dominant language
TypeScript
Stars
138
Forks
62
Avg merge
1d 4h
Merged PRs (30d)
35

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:

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.