From 42abfb1b853c80a1128795eb5c4e90b141834f6f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Feb 2026 22:29:07 +0100 Subject: [PATCH] Add createServiceMock to backend-test-utils Exports the internal simpleMock utility as createServiceMock, allowing plugin authors to define mock creators for their own service refs following the same pattern as the built-in mockServices mocks. Also updates catalog-node to use createServiceMock instead of its own copy, and simplifies the gateway-backend test to use the mock's .factory property directly. Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor Signed-off-by: Patrik Oldsberg --- .../backend-test-utils-create-service-mock.md | 5 ++ .../catalog-node-use-create-service-mock.md | 5 ++ .../backend-test-utils/report-alpha.api.md | 2 +- packages/backend-test-utils/report.api.md | 6 ++ .../services/ActionsRegistryServiceMock.ts | 16 ++--- .../src/alpha/services/ActionsServiceMock.ts | 9 +-- .../src/alpha/services/simpleMock.ts | 43 +----------- .../backend-test-utils/src/services/index.ts | 2 +- .../src/services/mockServices.ts | 64 +++++++++-------- .../src/services/simpleMock.ts | 39 ++++++++++- plugins/catalog-node/package.json | 8 +++ .../src/testUtils/catalogServiceMock.ts | 70 ++++++------------- plugins/gateway-backend/src/plugin.test.ts | 9 +-- yarn.lock | 5 ++ 14 files changed, 138 insertions(+), 145 deletions(-) create mode 100644 .changeset/backend-test-utils-create-service-mock.md create mode 100644 .changeset/catalog-node-use-create-service-mock.md diff --git a/.changeset/backend-test-utils-create-service-mock.md b/.changeset/backend-test-utils-create-service-mock.md new file mode 100644 index 0000000000..b35b647fd8 --- /dev/null +++ b/.changeset/backend-test-utils-create-service-mock.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': minor +--- + +Added `createServiceMock`, a public utility for creating `ServiceMock` instances for custom service refs. This allows plugin authors to define mock creators for their own services following the same pattern as the built-in `mockServices` mocks. diff --git a/.changeset/catalog-node-use-create-service-mock.md b/.changeset/catalog-node-use-create-service-mock.md new file mode 100644 index 0000000000..5032484ebb --- /dev/null +++ b/.changeset/catalog-node-use-create-service-mock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': patch +--- + +Updated `catalogServiceMock.mock` to use `createServiceMock` from `@backstage/backend-test-utils`, replacing the internal copy of `simpleMock`. Added `@backstage/backend-test-utils` as an optional peer dependency. diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md index ccbf96c0c7..ca6f47191d 100644 --- a/packages/backend-test-utils/report-alpha.api.md +++ b/packages/backend-test-utils/report-alpha.api.md @@ -70,7 +70,7 @@ export class MockActionsRegistry >(options: ActionsRegistryActionOptions): void; } -// @alpha (undocumented) +// @public (undocumented) export type ServiceMock = { factory: ServiceFactory; } & { diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index da5002275b..5c5f4d05ef 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -55,6 +55,12 @@ export interface CreateMockDirectoryOptions { mockOsTmpDir?: boolean; } +// @public +export function createServiceMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ServiceMock; + // @public (undocumented) export namespace mockCredentials { export function limitedUser( diff --git a/packages/backend-test-utils/src/alpha/services/ActionsRegistryServiceMock.ts b/packages/backend-test-utils/src/alpha/services/ActionsRegistryServiceMock.ts index faf89b393a..a89ad56757 100644 --- a/packages/backend-test-utils/src/alpha/services/ActionsRegistryServiceMock.ts +++ b/packages/backend-test-utils/src/alpha/services/ActionsRegistryServiceMock.ts @@ -16,11 +16,8 @@ import { mockServices } from '../../services'; import { LoggerService } from '@backstage/backend-plugin-api'; import { MockActionsRegistry } from './MockActionsRegistry'; -import { simpleMock } from './simpleMock'; -import { - ActionsRegistryService, - actionsRegistryServiceRef, -} from '@backstage/backend-plugin-api/alpha'; +import { createServiceMock } from './simpleMock'; +import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { actionsRegistryServiceFactory } from '@backstage/backend-defaults/alpha'; /** @@ -40,10 +37,7 @@ export function actionsRegistryServiceMock(options?: { export namespace actionsRegistryServiceMock { export const factory = () => actionsRegistryServiceFactory; - export const mock = simpleMock( - actionsRegistryServiceRef, - () => ({ - register: jest.fn(), - }), - ); + export const mock = createServiceMock(actionsRegistryServiceRef, () => ({ + register: jest.fn(), + })); } diff --git a/packages/backend-test-utils/src/alpha/services/ActionsServiceMock.ts b/packages/backend-test-utils/src/alpha/services/ActionsServiceMock.ts index 63abff4c37..d8c2f35f54 100644 --- a/packages/backend-test-utils/src/alpha/services/ActionsServiceMock.ts +++ b/packages/backend-test-utils/src/alpha/services/ActionsServiceMock.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { simpleMock } from './simpleMock'; -import { - ActionsService, - actionsServiceRef, -} from '@backstage/backend-plugin-api/alpha'; +import { createServiceMock } from './simpleMock'; +import { actionsServiceRef } from '@backstage/backend-plugin-api/alpha'; import { actionsServiceFactory } from '@backstage/backend-defaults/alpha'; /** @@ -27,7 +24,7 @@ import { actionsServiceFactory } from '@backstage/backend-defaults/alpha'; export namespace actionsServiceMock { export const factory = () => actionsServiceFactory; - export const mock = simpleMock(actionsServiceRef, () => ({ + export const mock = createServiceMock(actionsServiceRef, () => ({ invoke: jest.fn(), list: jest.fn(), })); diff --git a/packages/backend-test-utils/src/alpha/services/simpleMock.ts b/packages/backend-test-utils/src/alpha/services/simpleMock.ts index d07b143f7a..e1353d6e78 100644 --- a/packages/backend-test-utils/src/alpha/services/simpleMock.ts +++ b/packages/backend-test-utils/src/alpha/services/simpleMock.ts @@ -13,45 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - createServiceFactory, - ServiceFactory, - ServiceRef, -} from '@backstage/backend-plugin-api'; + +export { createServiceMock } from '../../services/simpleMock'; /** @alpha */ -export type ServiceMock = { - factory: ServiceFactory; -} & { - [Key in keyof TService]: TService[Key] extends ( - ...args: infer Args - ) => infer Return - ? TService[Key] & jest.MockInstance - : TService[Key]; -}; - -/** @internal */ -export function simpleMock( - ref: ServiceRef, - mockFactory: () => jest.Mocked, -): (partialImpl?: Partial) => ServiceMock { - return partialImpl => { - const mock = mockFactory(); - if (partialImpl) { - for (const [key, impl] of Object.entries(partialImpl)) { - if (typeof impl === 'function') { - (mock as any)[key].mockImplementation(impl); - } else { - (mock as any)[key] = impl; - } - } - } - return Object.assign(mock, { - factory: createServiceFactory({ - service: ref, - deps: {}, - factory: () => mock, - }), - }) as ServiceMock; - }; -} +export type { ServiceMock } from '../../services/simpleMock'; diff --git a/packages/backend-test-utils/src/services/index.ts b/packages/backend-test-utils/src/services/index.ts index 73f179ca41..e4d21fcdd5 100644 --- a/packages/backend-test-utils/src/services/index.ts +++ b/packages/backend-test-utils/src/services/index.ts @@ -15,4 +15,4 @@ */ export { mockServices } from './mockServices'; export { mockCredentials } from './mockCredentials'; -export { type ServiceMock } from './simpleMock'; +export { createServiceMock, type ServiceMock } from './simpleMock'; diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 9e941db3bb..8bb9b5610e 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -57,7 +57,7 @@ import { MockUserInfoService } from './MockUserInfoService'; import { mockCredentials } from './mockCredentials'; import { MockEventsService } from './MockEventsService'; import { MockPermissionsService } from './MockPermissionsService'; -import { simpleMock } from './simpleMock'; +import { createServiceMock } from './simpleMock'; import { MockSchedulerService } from './MockSchedulerService'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { ObservableConfigProxy } from '../../../config-loader/src/sources/ObservableConfigProxy'; @@ -159,7 +159,7 @@ export namespace mockServices { coreServices.rootConfig, rootConfig, ); - export const mock = simpleMock(coreServices.rootConfig, () => ({ + export const mock = createServiceMock(coreServices.rootConfig, () => ({ get: jest.fn(), getBoolean: jest.fn(), getConfig: jest.fn(), @@ -191,7 +191,7 @@ export namespace mockServices { coreServices.rootLogger, rootLogger, ); - export const mock = simpleMock(coreServices.rootLogger, () => ({ + export const mock = createServiceMock(coreServices.rootLogger, () => ({ child: jest.fn(), debug: jest.fn(), error: jest.fn(), @@ -203,7 +203,7 @@ export namespace mockServices { export namespace auditor { export const factory = () => auditorServiceFactory; - export const mock = simpleMock(coreServices.auditor, () => ({ + export const mock = createServiceMock(coreServices.auditor, () => ({ createEvent: jest.fn(async _ => { return { success: jest.fn(), @@ -242,7 +242,7 @@ export namespace mockServices { }); }, }); - export const mock = simpleMock(coreServices.auth, () => ({ + export const mock = createServiceMock(coreServices.auth, () => ({ authenticate: jest.fn(), getNoneCredentials: jest.fn(), getOwnServiceCredentials: jest.fn(), @@ -271,7 +271,7 @@ export namespace mockServices { deps: {}, factory: () => discovery(), }); - export const mock = simpleMock(coreServices.discovery, () => ({ + export const mock = createServiceMock(coreServices.discovery, () => ({ getBaseUrl: jest.fn(), getExternalBaseUrl: jest.fn(), })); @@ -320,7 +320,7 @@ export namespace mockServices { options?.defaultCredentials ?? mockCredentials.user(), ), }); - export const mock = simpleMock(coreServices.httpAuth, () => ({ + export const mock = createServiceMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), issueUserCookie: jest.fn(), })); @@ -353,7 +353,7 @@ export namespace mockServices { return new MockUserInfoService(); }, }); - export const mock = simpleMock(coreServices.userInfo, () => ({ + export const mock = createServiceMock(coreServices.userInfo, () => ({ getUserInfo: jest.fn(), })); } @@ -363,7 +363,7 @@ export namespace mockServices { // re-implement functioning mock versions here. export namespace cache { export const factory = () => cacheServiceFactory; - export const mock = simpleMock(coreServices.cache, () => ({ + export const mock = createServiceMock(coreServices.cache, () => ({ delete: jest.fn(), get: jest.fn(), set: jest.fn(), @@ -410,14 +410,14 @@ export namespace mockServices { * {@link @backstage/backend-plugin-api#coreServices.database}, optionally * with some given method implementations. */ - export const mock = simpleMock(coreServices.database, () => ({ + export const mock = createServiceMock(coreServices.database, () => ({ getClient: jest.fn(), })); } export namespace rootHealth { export const factory = () => rootHealthServiceFactory; - export const mock = simpleMock(coreServices.rootHealth, () => ({ + export const mock = createServiceMock(coreServices.rootHealth, () => ({ getLiveness: jest.fn(), getReadiness: jest.fn(), })); @@ -425,7 +425,7 @@ export namespace mockServices { export namespace httpRouter { export const factory = () => httpRouterServiceFactory; - export const mock = simpleMock(coreServices.httpRouter, () => ({ + export const mock = createServiceMock(coreServices.httpRouter, () => ({ use: jest.fn(), addAuthPolicy: jest.fn(), })); @@ -433,14 +433,14 @@ export namespace mockServices { export namespace rootHttpRouter { export const factory = () => rootHttpRouterServiceFactory(); - export const mock = simpleMock(coreServices.rootHttpRouter, () => ({ + export const mock = createServiceMock(coreServices.rootHttpRouter, () => ({ use: jest.fn(), })); } export namespace lifecycle { export const factory = () => lifecycleServiceFactory; - export const mock = simpleMock(coreServices.lifecycle, () => ({ + export const mock = createServiceMock(coreServices.lifecycle, () => ({ addShutdownHook: jest.fn(), addStartupHook: jest.fn(), })); @@ -448,7 +448,7 @@ export namespace mockServices { export namespace logger { export const factory = () => loggerServiceFactory; - export const mock = simpleMock(coreServices.logger, () => + export const mock = createServiceMock(coreServices.logger, () => createLoggerMock(), ); } @@ -484,7 +484,7 @@ export namespace mockServices { * {@link @backstage/backend-plugin-api#coreServices.permissions}, * optionally with some given method implementations. */ - export const mock = simpleMock(coreServices.permissions, () => ({ + export const mock = createServiceMock(coreServices.permissions, () => ({ authorize: jest.fn(), authorizeConditional: jest.fn(), })); @@ -492,17 +492,20 @@ export namespace mockServices { export namespace permissionsRegistry { export const factory = () => permissionsRegistryServiceFactory; - export const mock = simpleMock(coreServices.permissionsRegistry, () => ({ - addPermissionRules: jest.fn(), - addPermissions: jest.fn(), - addResourceType: jest.fn(), - getPermissionRuleset: jest.fn(), - })); + export const mock = createServiceMock( + coreServices.permissionsRegistry, + () => ({ + addPermissionRules: jest.fn(), + addPermissions: jest.fn(), + addResourceType: jest.fn(), + getPermissionRuleset: jest.fn(), + }), + ); } export namespace rootLifecycle { export const factory = () => rootLifecycleServiceFactory; - export const mock = simpleMock(coreServices.rootLifecycle, () => ({ + export const mock = createServiceMock(coreServices.rootLifecycle, () => ({ addShutdownHook: jest.fn(), addBeforeShutdownHook: jest.fn(), addStartupHook: jest.fn(), @@ -518,7 +521,7 @@ export namespace mockServices { includeManualTasksOnStartup?: boolean; includeInitialDelayedTasksOnStartup?: boolean; }) => new MockSchedulerService().factory(options); - export const mock = simpleMock(coreServices.scheduler, () => ({ + export const mock = createServiceMock(coreServices.scheduler, () => ({ createScheduledTaskRunner: jest.fn(), getScheduledTasks: jest.fn(), scheduleTask: jest.fn(), @@ -528,7 +531,7 @@ export namespace mockServices { export namespace urlReader { export const factory = () => urlReaderServiceFactory; - export const mock = simpleMock(coreServices.urlReader, () => ({ + export const mock = createServiceMock(coreServices.urlReader, () => ({ readTree: jest.fn(), readUrl: jest.fn(), search: jest.fn(), @@ -553,7 +556,7 @@ export namespace mockServices { * {@link @backstage/backend-events-node#eventsServiceRef}, optionally * with some given method implementations. */ - export const mock = simpleMock(eventsServiceRef, () => ({ + export const mock = createServiceMock(eventsServiceRef, () => ({ publish: jest.fn(), subscribe: jest.fn(), })); @@ -565,9 +568,12 @@ export namespace mockServices { }; } export namespace rootInstanceMetadata { - export const mock = simpleMock(coreServices.rootInstanceMetadata, () => ({ - getInstalledPlugins: jest.fn(), - })); + export const mock = createServiceMock( + coreServices.rootInstanceMetadata, + () => ({ + getInstalledPlugins: jest.fn(), + }), + ); export const factory = simpleFactoryWithOptions( coreServices.rootInstanceMetadata, rootInstanceMetadata, diff --git a/packages/backend-test-utils/src/services/simpleMock.ts b/packages/backend-test-utils/src/services/simpleMock.ts index 484969f235..5f71725dd4 100644 --- a/packages/backend-test-utils/src/services/simpleMock.ts +++ b/packages/backend-test-utils/src/services/simpleMock.ts @@ -30,8 +30,43 @@ export type ServiceMock = { : TService[Key]; }; -/** @internal */ -export function simpleMock( +/** + * Creates a standardized Backstage service mock factory function for producing + * mock service instances. + * + * @remarks + * + * Each method in the mock factory is a `jest.fn()`, and you can optionally pass + * partial implementations when calling the returned function. No type + * parameters should be provided to this function, they will be inferred from + * the provided service reference. + * + * The returned mock has a `.factory` property that can be passed directly to + * `startTestBackend` or other test utilities. + * + * @example + * ```ts + * import { createServiceMock } from '@backstage/backend-test-utils'; + * + * const myServiceMock = createServiceMock(myServiceRef, () => ({ + * doStuff: jest.fn(), + * doOtherStuff: jest.fn(), + * })); + * + * // Create a mock with default behavior: + * const mock = myServiceMock(); + * + * // Or with a partial implementation: + * const mock = myServiceMock({ doStuff: async () => 'test' }); + * expect(mock.doStuff).toHaveBeenCalledTimes(1); + * + * // It also has a `.factory` property for use with startTestBackend: + * await startTestBackend({ features: [mock.factory] }); + * ``` + * + * @public + */ +export function createServiceMock( ref: ServiceRef, mockFactory: () => jest.Mocked, ): (partialImpl?: Partial) => ServiceMock { diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 5b37153c43..99483bcc0d 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -71,6 +71,14 @@ "lodash": "^4.17.21", "yaml": "^2.0.0" }, + "peerDependencies": { + "@backstage/backend-test-utils": "workspace:^" + }, + "peerDependenciesMeta": { + "@backstage/backend-test-utils": { + "optional": true + } + }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts index 59c66c0b88..d413380747 100644 --- a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -17,41 +17,14 @@ import { createServiceFactory, ServiceFactory, - ServiceRef, } from '@backstage/backend-plugin-api'; import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; import { Entity } from '@backstage/catalog-model'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; // eslint-disable-next-line @backstage/no-undeclared-imports -import { ServiceMock } from '@backstage/backend-test-utils'; +import { createServiceMock } from '@backstage/backend-test-utils'; import { CatalogServiceMock } from './types'; -/** @internal */ -function simpleMock( - ref: ServiceRef, - mockFactory: () => jest.Mocked, -): (partialImpl?: Partial) => ServiceMock { - return partialImpl => { - const mock = mockFactory(); - if (partialImpl) { - for (const [key, impl] of Object.entries(partialImpl)) { - if (typeof impl === 'function') { - (mock as any)[key].mockImplementation(impl); - } else { - (mock as any)[key] = impl; - } - } - } - return Object.assign(mock, { - factory: createServiceFactory({ - service: ref, - deps: {}, - factory: () => mock, - }), - }) as ServiceMock; - }; -} - /** * Creates a fake catalog client that handles entities in memory storage. Note * that this client may be severely limited in functionality, and advanced @@ -86,23 +59,26 @@ export namespace catalogServiceMock { * Creates a catalog client whose methods are mock functions, possibly with * some of them overloaded by the caller. */ - export const mock = simpleMock(catalogServiceRef, () => ({ - getEntities: jest.fn(), - getEntitiesByRefs: jest.fn(), - queryEntities: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityByRef: jest.fn(), - removeEntityByUid: jest.fn(), - refreshEntity: jest.fn(), - getEntityFacets: jest.fn(), - getLocations: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - getLocationByEntity: jest.fn(), - validateEntity: jest.fn(), - analyzeLocation: jest.fn(), - streamEntities: jest.fn(), - })); + export const mock = createServiceMock( + catalogServiceRef, + () => ({ + getEntities: jest.fn(), + getEntitiesByRefs: jest.fn(), + queryEntities: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), + getEntityFacets: jest.fn(), + getLocations: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + getLocationByEntity: jest.fn(), + validateEntity: jest.fn(), + analyzeLocation: jest.fn(), + streamEntities: jest.fn(), + }), + ); } diff --git a/plugins/gateway-backend/src/plugin.test.ts b/plugins/gateway-backend/src/plugin.test.ts index 8db99e39a5..0b2eda838e 100644 --- a/plugins/gateway-backend/src/plugin.test.ts +++ b/plugins/gateway-backend/src/plugin.test.ts @@ -19,7 +19,6 @@ import { mockServices } from '@backstage/backend-test-utils'; import { coreServices, createBackendPlugin, - createServiceFactory, } from '@backstage/backend-plugin-api'; import { Router } from 'express'; import { EventSource } from 'eventsource'; @@ -68,13 +67,7 @@ describe('gateway', () => { ); backend.add(mockServices.auth.factory()); backend.add(mockServices.httpAuth.factory()); - backend.add( - createServiceFactory({ - service: coreServices.discovery, - deps: {}, - factory: () => discovery, - }), - ); + backend.add(discovery.factory); backend.add(dummyPlugin); backend.add(import('./')); diff --git a/yarn.lock b/yarn.lock index f90a8202c4..b94fae61ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5399,6 +5399,11 @@ __metadata: lodash: "npm:^4.17.21" msw: "npm:^1.0.0" yaml: "npm:^2.0.0" + peerDependencies: + "@backstage/backend-test-utils": "workspace:^" + peerDependenciesMeta: + "@backstage/backend-test-utils": + optional: true languageName: unknown linkType: soft