diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index b2c38ab506..4bc60f557f 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -17,3 +17,4 @@ export * from './errors'; export * from './logging'; export * from './middleware'; +export * from './testing'; diff --git a/packages/backend-common/src/testing/MockedMemberFunctions.ts b/packages/backend-common/src/testing/MockedMemberFunctions.ts new file mode 100644 index 0000000000..9b35908bff --- /dev/null +++ b/packages/backend-common/src/testing/MockedMemberFunctions.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * For any type T, generate a new type that is identical but also has the + * jest.fn signature on all member functions. + * + * When writing tests against a type, you sometimes end up in a situation where + * you need to write expect(x.y as jest.Mock).toHaveBeenCalled... because the + * x.y member was considered to be the actual function type. You could also + * change your test to instead create a "raw" object { y: jest.fn() } but then + * you lose type safety when doing x.y.mockReturnValue(...). So you start + * trying to do { y: jest.fn() as X['y'] } as X or similar trickery. + * + * This type lets you say const x: MockedMemberFunctions = { y: jest.fn() } + * and keep all the type safety at every step. + */ +export type MockedMemberFunctions = { + [K in keyof T]: T[K] extends (...args: infer A) => infer B + ? T[K] & jest.Mock + : T[K]; +}; diff --git a/packages/backend-common/src/testing/index.ts b/packages/backend-common/src/testing/index.ts new file mode 100644 index 0000000000..c12297465b --- /dev/null +++ b/packages/backend-common/src/testing/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { MockedMemberFunctions } from './MockedMemberFunctions'; diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index d897584c95..c5101dff7a 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -14,20 +14,31 @@ * limitations under the License. */ +import type { MockedMemberFunctions } from '@backstage/backend-common'; import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import type { Database } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { - let db: Database; + let db: MockedMemberFunctions; let policy: EntityPolicy; beforeEach(() => { - // Since the database has a large API surface, we just leave it empty and - // let the tests insert whatever methods they need to call - db = ({ - transaction: jest.fn(async f => f('mock_tx')), - } as unknown) as Database; + db = { + transaction: jest.fn(), + addEntity: jest.fn(), + updateEntity: jest.fn(), + entities: jest.fn(), + entity: jest.fn(), + removeEntity: jest.fn(), + addLocation: jest.fn(), + removeLocation: jest.fn(), + location: jest.fn(), + locations: jest.fn(), + locationHistory: jest.fn(), + addLocationUpdateLogEvent: jest.fn(), + }; + db.transaction.mockImplementation(async f => f('tx')); policy = { enforce: jest.fn(async x => x) }; }); @@ -42,8 +53,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([]); - db.addEntity = jest.fn().mockResolvedValue({ entity }); + db.entities.mockResolvedValue([]); + db.addEntity.mockResolvedValue({ entity }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(entity); @@ -65,8 +76,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([]); - db.updateEntity = jest.fn().mockResolvedValue({ entity }); + db.entities.mockResolvedValue([]); + db.updateEntity.mockResolvedValue({ entity }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(entity); @@ -95,8 +106,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([{ entity: existing }]); - db.updateEntity = jest.fn().mockResolvedValue({ entity: added }); + db.entities.mockResolvedValue([{ entity: existing }]); + db.updateEntity.mockResolvedValue({ entity: added }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(added);