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 <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-09 22:29:07 +01:00
parent 9848734ce6
commit 42abfb1b85
14 changed files with 138 additions and 145 deletions
@@ -70,7 +70,7 @@ export class MockActionsRegistry
>(options: ActionsRegistryActionOptions<TInputSchema, TOutputSchema>): void;
}
// @alpha (undocumented)
// @public (undocumented)
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
} & {
@@ -55,6 +55,12 @@ export interface CreateMockDirectoryOptions {
mockOsTmpDir?: boolean;
}
// @public
export function createServiceMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService>;
// @public (undocumented)
export namespace mockCredentials {
export function limitedUser(
@@ -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<ActionsRegistryService>(
actionsRegistryServiceRef,
() => ({
register: jest.fn(),
}),
);
export const mock = createServiceMock(actionsRegistryServiceRef, () => ({
register: jest.fn(),
}));
}
@@ -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<ActionsService>(actionsServiceRef, () => ({
export const mock = createServiceMock(actionsServiceRef, () => ({
invoke: jest.fn(),
list: jest.fn(),
}));
@@ -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<TService> = {
factory: ServiceFactory<TService>;
} & {
[Key in keyof TService]: TService[Key] extends (
...args: infer Args
) => infer Return
? TService[Key] & jest.MockInstance<Return, Args>
: TService[Key];
};
/** @internal */
export function simpleMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {
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<TService>;
};
}
export type { ServiceMock } from '../../services/simpleMock';
@@ -15,4 +15,4 @@
*/
export { mockServices } from './mockServices';
export { mockCredentials } from './mockCredentials';
export { type ServiceMock } from './simpleMock';
export { createServiceMock, type ServiceMock } from './simpleMock';
@@ -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,
@@ -30,8 +30,43 @@ export type ServiceMock<TService> = {
: TService[Key];
};
/** @internal */
export function simpleMock<TService>(
/**
* 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<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {