Skip to content

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

@karthiknadig

Description

@karthiknadig

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:

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.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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions