Skip to content

Commit 2cd8bc7

Browse files
committed
test mocking
1 parent ecc92c4 commit 2cd8bc7

File tree

2 files changed

+169
-29
lines changed

2 files changed

+169
-29
lines changed

src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { EXTENSION_ROOT_DIR } from '../../../../client/constants';
2222
import { MockChildProcess } from '../../../mocks/mockChildProcess';
2323
import { traceInfo } from '../../../../client/logging';
2424
import * as extapi from '../../../../client/envExt/api.internal';
25-
import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter';
25+
import { createMockProjectAdapter } from '../testMocks';
2626

2727
suite('pytest test execution adapter', () => {
2828
let useEnvExtensionStub: sinon.SinonStub;
@@ -330,27 +330,6 @@ suite('pytest test execution adapter', () => {
330330
// ===== PROJECT-BASED EXECUTION TESTS =====
331331

332332
suite('project-based execution', () => {
333-
function createMockProjectAdapter(projectPath: string, projectName: string): ProjectAdapter {
334-
return ({
335-
projectUri: Uri.file(projectPath),
336-
projectName,
337-
workspaceUri: Uri.file(projectPath),
338-
testProvider: 'pytest',
339-
pythonEnvironment: {
340-
execInfo: { run: { executable: '/custom/python/path' } },
341-
},
342-
pythonProject: {
343-
name: projectName,
344-
uri: Uri.file(projectPath),
345-
},
346-
executionAdapter: {},
347-
discoveryAdapter: {},
348-
resultResolver: {},
349-
isDiscovering: false,
350-
isExecuting: false,
351-
} as unknown) as ProjectAdapter;
352-
}
353-
354333
test('should set PROJECT_ROOT_PATH env var when project provided', async () => {
355334
const deferred2 = createDeferred();
356335
const deferred3 = createDeferred();
@@ -369,7 +348,11 @@ suite('pytest test execution adapter', () => {
369348
testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any));
370349

371350
const projectPath = path.join('/', 'workspace', 'myproject');
372-
const mockProject = createMockProjectAdapter(projectPath, 'myproject');
351+
const mockProject = createMockProjectAdapter({
352+
projectPath,
353+
projectName: 'myproject',
354+
pythonPath: '/custom/python/path',
355+
});
373356

374357
const uri = Uri.file(myTestPath);
375358
adapter = new PytestTestExecutionAdapter(configService);
@@ -427,7 +410,11 @@ suite('pytest test execution adapter', () => {
427410
);
428411

429412
const projectPath = path.join('/', 'workspace', 'myproject');
430-
const mockProject = createMockProjectAdapter(projectPath, 'myproject (Python 3.11)');
413+
const mockProject = createMockProjectAdapter({
414+
projectPath,
415+
projectName: 'myproject (Python 3.11)',
416+
pythonPath: '/custom/python/path',
417+
});
431418

432419
const uri = Uri.file(myTestPath);
433420
adapter = new PytestTestExecutionAdapter(configService);
@@ -448,8 +435,10 @@ suite('pytest test execution adapter', () => {
448435
(x) =>
449436
x.launchDebugger(
450437
typeMoq.It.is<LaunchOptions>((launchOptions) => {
451-
assert.equal(launchOptions.debugSessionName, 'myproject (Python 3.11)');
452-
assert.equal(launchOptions.pythonPath, '/custom/python/path');
438+
// Project should be passed for project-based debugging
439+
assert.ok(launchOptions.project, 'project should be defined');
440+
assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)');
441+
assert.equal(launchOptions.project?.uri.fsPath, projectPath);
453442
return true;
454443
}),
455444
typeMoq.It.isAny(),
@@ -499,7 +488,7 @@ suite('pytest test execution adapter', () => {
499488
);
500489
});
501490

502-
test('should not set debugSessionName or pythonPath in LaunchOptions when no project', async () => {
491+
test('should not set project in LaunchOptions when no project provided', async () => {
503492
const deferred3 = createDeferred();
504493
utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName'));
505494

@@ -533,8 +522,7 @@ suite('pytest test execution adapter', () => {
533522
(x) =>
534523
x.launchDebugger(
535524
typeMoq.It.is<LaunchOptions>((launchOptions) => {
536-
assert.equal(launchOptions.debugSessionName, undefined);
537-
assert.equal(launchOptions.pythonPath, undefined);
525+
assert.equal(launchOptions.project, undefined);
538526
return true;
539527
}),
540528
typeMoq.It.isAny(),
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Centralized mock utilities for testing testController components.
6+
* Re-use these helpers across multiple test files for consistency.
7+
*/
8+
9+
import * as sinon from 'sinon';
10+
import * as typemoq from 'typemoq';
11+
import { TestItem, TestItemCollection, TestRun, Uri } from 'vscode';
12+
import { IPythonExecutionFactory } from '../../../client/common/process/types';
13+
import { ITestDebugLauncher } from '../../../client/testing/common/types';
14+
import { ProjectAdapter } from '../../../client/testing/testController/common/projectAdapter';
15+
import { ProjectExecutionDependencies } from '../../../client/testing/testController/common/projectTestExecution';
16+
import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry';
17+
import { ITestExecutionAdapter, ITestResultResolver } from '../../../client/testing/testController/common/types';
18+
19+
/**
20+
* Creates a mock TestItem with configurable properties.
21+
* @param id - The unique ID of the test item
22+
* @param uriPath - The file path for the test item's URI
23+
* @param children - Optional array of child test items
24+
*/
25+
export function createMockTestItem(id: string, uriPath: string, children?: TestItem[]): TestItem {
26+
const childMap = new Map<string, TestItem>();
27+
children?.forEach((c) => childMap.set(c.id, c));
28+
29+
const mockChildren: TestItemCollection = {
30+
size: childMap.size,
31+
forEach: (callback: (item: TestItem, collection: TestItemCollection) => void) => {
32+
childMap.forEach((item) => callback(item, mockChildren));
33+
},
34+
get: (itemId: string) => childMap.get(itemId),
35+
add: () => {},
36+
delete: () => {},
37+
replace: () => {},
38+
[Symbol.iterator]: function* () {
39+
for (const [key, value] of childMap) {
40+
yield [key, value] as [string, TestItem];
41+
}
42+
},
43+
} as TestItemCollection;
44+
45+
return ({
46+
id,
47+
uri: Uri.file(uriPath),
48+
children: mockChildren,
49+
label: id,
50+
canResolveChildren: false,
51+
busy: false,
52+
tags: [],
53+
range: undefined,
54+
error: undefined,
55+
parent: undefined,
56+
} as unknown) as TestItem;
57+
}
58+
59+
/**
60+
* Creates a mock TestItem without a URI.
61+
* Useful for testing edge cases where test items have no associated file.
62+
* @param id - The unique ID of the test item
63+
*/
64+
export function createMockTestItemWithoutUri(id: string): TestItem {
65+
return ({
66+
id,
67+
uri: undefined,
68+
children: ({ size: 0, forEach: () => {} } as unknown) as TestItemCollection,
69+
label: id,
70+
} as unknown) as TestItem;
71+
}
72+
73+
export interface MockProjectAdapterConfig {
74+
projectPath: string;
75+
projectName: string;
76+
pythonPath?: string;
77+
testProvider?: 'pytest' | 'unittest';
78+
}
79+
80+
export type MockProjectAdapter = ProjectAdapter & { executionAdapterStub: sinon.SinonStub };
81+
82+
/**
83+
* Creates a mock ProjectAdapter for testing project-based test execution.
84+
* @param config - Configuration object with project details
85+
* @returns A mock ProjectAdapter with an exposed executionAdapterStub for verification
86+
*/
87+
export function createMockProjectAdapter(config: MockProjectAdapterConfig): MockProjectAdapter {
88+
const runTestsStub = sinon.stub().resolves();
89+
const executionAdapter: ITestExecutionAdapter = ({
90+
runTests: runTestsStub,
91+
} as unknown) as ITestExecutionAdapter;
92+
93+
const resultResolverMock: ITestResultResolver = ({
94+
vsIdToRunId: new Map<string, string>(),
95+
runIdToVSid: new Map<string, string>(),
96+
runIdToTestItem: new Map<string, TestItem>(),
97+
detailedCoverageMap: new Map(),
98+
resolveDiscovery: () => Promise.resolve(),
99+
resolveExecution: () => {},
100+
} as unknown) as ITestResultResolver;
101+
102+
const adapter = ({
103+
projectUri: Uri.file(config.projectPath),
104+
projectName: config.projectName,
105+
workspaceUri: Uri.file(config.projectPath),
106+
testProvider: config.testProvider ?? 'pytest',
107+
pythonEnvironment: config.pythonPath
108+
? {
109+
execInfo: { run: { executable: config.pythonPath } },
110+
}
111+
: undefined,
112+
pythonProject: {
113+
name: config.projectName,
114+
uri: Uri.file(config.projectPath),
115+
},
116+
executionAdapter,
117+
discoveryAdapter: {} as any,
118+
resultResolver: resultResolverMock,
119+
isDiscovering: false,
120+
isExecuting: false,
121+
// Expose the stub for testing
122+
executionAdapterStub: runTestsStub,
123+
} as unknown) as MockProjectAdapter;
124+
125+
return adapter;
126+
}
127+
128+
/**
129+
* Creates mock dependencies for project test execution.
130+
* @returns An object containing mocked ProjectExecutionDependencies
131+
*/
132+
export function createMockDependencies(): ProjectExecutionDependencies {
133+
return {
134+
projectRegistry: typemoq.Mock.ofType<TestProjectRegistry>().object,
135+
pythonExecFactory: typemoq.Mock.ofType<IPythonExecutionFactory>().object,
136+
debugLauncher: typemoq.Mock.ofType<ITestDebugLauncher>().object,
137+
};
138+
}
139+
140+
/**
141+
* Creates a mock TestRun with common setup methods.
142+
* @returns A TypeMoq mock of TestRun
143+
*/
144+
export function createMockTestRun(): typemoq.IMock<TestRun> {
145+
const runMock = typemoq.Mock.ofType<TestRun>();
146+
runMock.setup((r) => r.started(typemoq.It.isAny()));
147+
runMock.setup((r) => r.passed(typemoq.It.isAny(), typemoq.It.isAny()));
148+
runMock.setup((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny()));
149+
runMock.setup((r) => r.skipped(typemoq.It.isAny()));
150+
runMock.setup((r) => r.end());
151+
return runMock;
152+
}

0 commit comments

Comments
 (0)