microsoft / microsoft/vscode-python-environments

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

未关闭
#1,159 1 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

@karthiknadig 已经在做这个了。

开始于 2026年2月3日。

enhancement testing
主要语言
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:

  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

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。