Skip to content

Commit 2a43ffb

Browse files
committed
Add new mockedObject function
This will remove some instances where we're using `as unknown as T` and replace them by a call to `mockedObject<T>()`. The `mockedObject` function is a bit more explicit about what it does and has types which ensure that the methods that are set on the object actually exist. Unfortunately, we can't fully get rid of `as unknown as T` in the `mockedObject` function. However, this construct is more localized and does not need to be used in as many places. If we do enable an ESLint rule to prevent the use of `as unknown as T`, I would feel comfortable with disabling the rule for the `mockedObject` function.
1 parent 396fdb8 commit 2a43ffb

File tree

9 files changed

+84
-49
lines changed

9 files changed

+84
-49
lines changed

extensions/ql-vscode/src/pure/bqrs-cli-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function transformBqrsResultSet(
105105
};
106106
}
107107

108-
type BqrsKind =
108+
export type BqrsKind =
109109
| "String"
110110
| "Float"
111111
| "Integer"

extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { join } from "path";
44
import { CancellationToken, ExtensionContext, Uri, workspace } from "vscode";
55

66
import {
7+
DatabaseContents,
78
DatabaseEventKind,
8-
DatabaseManager,
99
DatabaseItemImpl,
10-
DatabaseContents,
11-
FullDatabaseOptions,
12-
findSourceArchive,
10+
DatabaseManager,
1311
DatabaseResolver,
12+
findSourceArchive,
13+
FullDatabaseOptions,
1414
} from "../../../src/local-databases";
1515
import { Logger } from "../../../src/common";
1616
import { ProgressCallback } from "../../../src/commandRunner";
@@ -24,6 +24,7 @@ import { QueryRunner } from "../../../src/queryRunner";
2424
import * as helpers from "../../../src/helpers";
2525
import { Setting } from "../../../src/config";
2626
import { QlPackGenerator } from "../../../src/qlpack-generator";
27+
import { mockedObject } from "../utils/mocking.helpers";
2728

2829
describe("local databases", () => {
2930
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
@@ -77,20 +78,20 @@ describe("local databases", () => {
7778

7879
databaseManager = new DatabaseManager(
7980
extensionContext,
80-
{
81+
mockedObject<QueryRunner>({
8182
registerDatabase: registerSpy,
8283
deregisterDatabase: deregisterSpy,
8384
onStart: () => {
8485
/**/
8586
},
86-
} as unknown as QueryRunner,
87-
{
87+
}),
88+
mockedObject<CodeQLCliServer>({
8889
resolveDatabase: resolveDatabaseSpy,
8990
packAdd: packAddSpy,
90-
} as unknown as CodeQLCliServer,
91-
{
91+
}),
92+
mockedObject<Logger>({
9293
log: logSpy,
93-
} as unknown as Logger,
94+
}),
9495
);
9596

9697
// Unfortunately, during a test it is not possible to convert from

extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CodeQLCliServer } from "../../../src/cli";
66
import { Uri, workspace } from "vscode";
77
import { getErrorMessage } from "../../../src/pure/helpers-pure";
88
import * as tmp from "tmp";
9+
import { mockedObject } from "../utils/mocking.helpers";
910

1011
describe("QlPackGenerator", () => {
1112
let packFolderName: string;
@@ -14,7 +15,7 @@ describe("QlPackGenerator", () => {
1415
let exampleQlFilePath: string;
1516
let language: string;
1617
let generator: QlPackGenerator;
17-
let packAddSpy: jest.SpyInstance;
18+
let packAddSpy: jest.Mock<any, []>;
1819
let dir: tmp.DirResult;
1920

2021
beforeEach(async () => {
@@ -28,9 +29,9 @@ describe("QlPackGenerator", () => {
2829
exampleQlFilePath = join(packFolderPath, "example.ql");
2930

3031
packAddSpy = jest.fn();
31-
const mockCli = {
32+
const mockCli = mockedObject<CodeQLCliServer>({
3233
packAdd: packAddSpy,
33-
} as unknown as CodeQLCliServer;
34+
});
3435

3536
generator = new QlPackGenerator(
3637
packFolderName,

extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/astBuilder.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CodeQLCliServer } from "../../../../src/cli";
55
import { DatabaseItem } from "../../../../src/local-databases";
66
import { Uri } from "vscode";
77
import { QueryWithResults } from "../../../../src/run-queries-shared";
8+
import { mockedObject } from "../../utils/mocking.helpers";
89

910
/**
1011
*
@@ -32,15 +33,15 @@ describe("AstBuilder", () => {
3233
let overrides: Record<string, Record<string, unknown> | undefined>;
3334

3435
beforeEach(() => {
35-
mockCli = {
36+
mockCli = mockedObject<CodeQLCliServer>({
3637
bqrsDecode: jest
3738
.fn()
3839
.mockImplementation(
3940
(_: string, resultSet: "nodes" | "edges" | "graphProperties") => {
4041
return mockDecode(resultSet);
4142
},
4243
),
43-
} as unknown as CodeQLCliServer;
44+
});
4445
overrides = {
4546
nodes: undefined,
4647
edges: undefined,

extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/queryResolver.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "../../../../src/contextual/queryResolver";
1212
import { CodeQLCliServer } from "../../../../src/cli";
1313
import { DatabaseItem } from "../../../../src/local-databases";
14+
import { mockedObject } from "../../utils/mocking.helpers";
1415

1516
describe("queryResolver", () => {
1617
let getQlPackForDbschemeSpy: jest.SpiedFunction<
@@ -20,9 +21,11 @@ describe("queryResolver", () => {
2021
typeof helpers.getPrimaryDbscheme
2122
>;
2223

23-
const mockCli = {
24-
resolveQueriesInSuite: jest.fn(),
25-
};
24+
const resolveQueriesInSuite = jest.fn();
25+
26+
const mockCli = mockedObject<CodeQLCliServer>({
27+
resolveQueriesInSuite,
28+
});
2629

2730
beforeEach(() => {
2831
getQlPackForDbschemeSpy = jest
@@ -41,20 +44,20 @@ describe("queryResolver", () => {
4144

4245
describe("resolveQueries", () => {
4346
it("should resolve a query", async () => {
44-
mockCli.resolveQueriesInSuite.mockReturnValue(["a", "b"]);
47+
resolveQueriesInSuite.mockReturnValue(["a", "b"]);
4548
const result = await resolveQueries(
46-
mockCli as unknown as CodeQLCliServer,
49+
mockCli,
4750
{ dbschemePack: "my-qlpack", dbschemePackIsLibraryPack: false },
4851
KeyType.DefinitionQuery,
4952
);
5053
expect(result).toEqual(["a", "b"]);
5154

52-
expect(mockCli.resolveQueriesInSuite).toHaveBeenCalledWith(
55+
expect(resolveQueriesInSuite).toHaveBeenCalledWith(
5356
expect.stringMatching(/\.qls$/),
5457
[],
5558
);
5659

57-
const fileName = mockCli.resolveQueriesInSuite.mock.calls[0][0];
60+
const fileName = resolveQueriesInSuite.mock.calls[0][0];
5861

5962
expect(load(await fs.readFile(fileName, "utf-8"))).toEqual([
6063
{
@@ -69,11 +72,11 @@ describe("queryResolver", () => {
6972
});
7073

7174
it("should throw an error when there are no queries found", async () => {
72-
mockCli.resolveQueriesInSuite.mockReturnValue([]);
75+
resolveQueriesInSuite.mockReturnValue([]);
7376

7477
try {
7578
await resolveQueries(
76-
mockCli as unknown as CodeQLCliServer,
79+
mockCli,
7780
{ dbschemePack: "my-qlpack", dbschemePackIsLibraryPack: false },
7881
KeyType.DefinitionQuery,
7982
);
@@ -100,10 +103,7 @@ describe("queryResolver", () => {
100103
},
101104
},
102105
} as unknown as DatabaseItem;
103-
const result = await qlpackOfDatabase(
104-
mockCli as unknown as CodeQLCliServer,
105-
db,
106-
);
106+
const result = await qlpackOfDatabase(mockCli, db);
107107
expect(result).toEqual({
108108
dbschemePack: "my-qlpack",
109109
dbschemePackIsLibraryPack: false,

extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
QueryResultType,
3131
} from "../../../src/pure/legacy-messages";
3232
import { sleep } from "../../../src/pure/time";
33+
import { mockedObject } from "../utils/mocking.helpers";
3334

3435
describe("query-results", () => {
3536
let queryPath: string;
@@ -139,9 +140,9 @@ describe("query-results", () => {
139140
const completedQuery = fqi.completedQuery!;
140141

141142
const spy = jest.fn();
142-
const mockServer = {
143+
const mockServer = mockedObject<CodeQLCliServer>({
143144
sortBqrs: spy,
144-
} as unknown as CodeQLCliServer;
145+
});
145146
const sortState = {
146147
columnIndex: 1,
147148
sortDirection: SortDirection.desc,
@@ -196,9 +197,9 @@ describe("query-results", () => {
196197

197198
await ensureDir(basename(interpretedResultsPath));
198199

199-
mockServer = {
200+
mockServer = mockedObject<CodeQLCliServer>({
200201
interpretBqrsSarif: spy,
201-
} as unknown as CodeQLCliServer;
202+
});
202203
});
203204

204205
afterEach(async () => {

extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
1616
import { QueryInProgress } from "../../../src/legacy-query-server/run-queries";
1717
import { LegacyQueryRunner } from "../../../src/legacy-query-server/legacyRunner";
1818
import { DatabaseItem } from "../../../src/local-databases";
19+
import { DeepPartial, mockedObject } from "../utils/mocking.helpers";
20+
import { BqrsKind } from "../../../src/pure/bqrs-cli-types";
1921

2022
describe("run-queries", () => {
2123
let isCanarySpy: jest.SpiedFunction<typeof config.isCanary>;
@@ -77,7 +79,7 @@ describe("run-queries", () => {
7779
],
7880
bqrsDecode: [
7981
{
80-
columns: [{ kind: "NotString" }, { kind: "String" }],
82+
columns: [{ kind: "NotString" as BqrsKind }, { kind: "String" }],
8183
tuples: [
8284
["a", "b"],
8385
["c", "d"],
@@ -89,8 +91,8 @@ describe("run-queries", () => {
8991
// this won't happen with the real CLI, but it's a good test
9092
columns: [
9193
{ kind: "String" },
92-
{ kind: "NotString" },
93-
{ kind: "StillNotString" },
94+
{ kind: "NotString" as BqrsKind },
95+
{ kind: "StillNotString" as BqrsKind },
9496
],
9597
tuples: [["a", "b", "c"]],
9698
},
@@ -125,7 +127,7 @@ describe("run-queries", () => {
125127
],
126128
bqrsDecode: [
127129
{
128-
columns: [{ kind: "NotString" }, { kind: "String" }],
130+
columns: [{ kind: "NotString" as BqrsKind }, { kind: "String" }],
129131
// We only escape string columns. In practice, we will only see quotes in strings, but
130132
// it is a good test anyway.
131133
tuples: [
@@ -312,7 +314,7 @@ describe("run-queries", () => {
312314
function createMockQueryServerClient(
313315
cliServer?: CodeQLCliServer,
314316
): QueryServerClient {
315-
return {
317+
return mockedObject<QueryServerClient>({
316318
config: {
317319
timeoutSecs: 5,
318320
},
@@ -326,20 +328,32 @@ describe("run-queries", () => {
326328
log: jest.fn(),
327329
},
328330
cliServer,
329-
} as unknown as QueryServerClient;
331+
});
330332
}
331333

334+
// A type that represents the mocked methods of a CodeQLCliServer. Exclude any non-methods.
335+
// This allows passing in an array of return values for a single method.
336+
type MockedCLIMethods = {
337+
[K in keyof CodeQLCliServer]: CodeQLCliServer[K] extends (
338+
...args: any
339+
) => any
340+
? Array<DeepPartial<Awaited<ReturnType<CodeQLCliServer[K]>>>>
341+
: never;
342+
};
343+
332344
function createMockCliServer(
333-
mockOperations: Record<string, any[]>,
345+
mockOperations: Partial<MockedCLIMethods>,
334346
): CodeQLCliServer {
335-
const mockServer: Record<string, any> = {};
347+
const mockedMethods: Record<string, jest.Mock> = {};
348+
336349
for (const [operation, returns] of Object.entries(mockOperations)) {
337-
mockServer[operation] = jest.fn();
338-
returns.forEach((returnValue) => {
339-
mockServer[operation].mockResolvedValueOnce(returnValue);
350+
const fn = jest.fn();
351+
returns.forEach((returnValue: any) => {
352+
fn.mockResolvedValueOnce(returnValue);
340353
});
354+
mockedMethods[operation] = fn;
341355
}
342356

343-
return mockServer as unknown as CodeQLCliServer;
357+
return mockedObject<CodeQLCliServer>(mockedMethods);
344358
}
345359
});

extensions/ql-vscode/test/vscode-tests/no-workspace/test-adapter.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DatabaseManager,
1010
FullDatabaseOptions,
1111
} from "../../../src/local-databases";
12+
import { mockedObject } from "../utils/mocking.helpers";
1213

1314
jest.mock("fs-extra", () => {
1415
const original = jest.requireActual("fs-extra");
@@ -74,15 +75,15 @@ describe("test-adapter", () => {
7475
jest.spyOn(preTestDatabaseItem, "isAffectedByTest").mockResolvedValue(true);
7576

7677
adapter = new QLTestAdapter(
77-
{
78+
mockedObject<WorkspaceFolder>({
7879
name: "ABC",
7980
uri: Uri.parse("file:/ab/c"),
80-
} as WorkspaceFolder,
81-
{
81+
}),
82+
mockedObject<CodeQLCliServer>({
8283
runTests: runTestsSpy,
8384
resolveQlpacks: resolveQlpacksSpy,
8485
resolveTests: resolveTestsSpy,
85-
} as unknown as CodeQLCliServer,
86+
}),
8687
fakeDatabaseManager,
8788
);
8889
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type DeepPartial<T> = T extends object
2+
? {
3+
[P in keyof T]?: DeepPartial<T[P]>;
4+
}
5+
: T;
6+
7+
export function mockedObject<T extends object>(methods: DeepPartial<T>): T {
8+
return new Proxy<T>({} as unknown as T, {
9+
get: (_target, prop) => {
10+
if (prop in methods) {
11+
return (methods as any)[prop];
12+
}
13+
throw new Error(`Method ${String(prop)} not mocked`);
14+
},
15+
});
16+
}

0 commit comments

Comments
 (0)