diff --git a/.changeset/old-geese-drive.md b/.changeset/old-geese-drive.md
new file mode 100644
index 0000000000..ddaa7ad30a
--- /dev/null
+++ b/.changeset/old-geese-drive.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-test-utils': patch
+---
+
+Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods.
diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md
index d086b1d2ae..f87181f635 100644
--- a/packages/backend-test-utils/api-report.md
+++ b/packages/backend-test-utils/api-report.md
@@ -3,24 +3,52 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+///
+
+import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common';
import { Backend } from '@backstage/backend-app-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
+import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
+import { CacheServiceOptions } from '@backstage/backend-plugin-api';
+import { CacheServiceSetOptions } from '@backstage/backend-plugin-api';
import { DatabaseService } from '@backstage/backend-plugin-api';
+import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common';
+import { EvaluatorRequestOptions } from '@backstage/plugin-permission-common';
import { ExtendedHttpServer } from '@backstage/backend-app-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
+import { Handler } from 'express';
import { HttpRouterFactoryOptions } from '@backstage/backend-app-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
+import { IdentityApiGetIdentityRequest } from '@backstage/plugin-auth-node';
import { IdentityService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
+import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { LifecycleService } from '@backstage/backend-plugin-api';
+import { LifecycleServiceShutdownHook } from '@backstage/backend-plugin-api';
+import { LifecycleServiceShutdownOptions } from '@backstage/backend-plugin-api';
+import { LifecycleServiceStartupHook } from '@backstage/backend-plugin-api';
+import { LifecycleServiceStartupOptions } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
+import { PolicyDecision } from '@backstage/plugin-permission-common';
+import { QueryPermissionRequest } from '@backstage/plugin-permission-common';
+import { ReadTreeOptions } from '@backstage/backend-plugin-api';
+import { ReadTreeResponse } from '@backstage/backend-plugin-api';
+import { ReadUrlOptions } from '@backstage/backend-plugin-api';
+import { ReadUrlResponse } from '@backstage/backend-plugin-api';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
+import { RootLoggerService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
+import { SearchOptions } from '@backstage/backend-plugin-api';
+import { SearchResponse } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
+import { TaskDescriptor } from '@backstage/backend-tasks';
+import { TaskInvocationDefinition } from '@backstage/backend-tasks';
+import { TaskRunner } from '@backstage/backend-tasks';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
@@ -33,11 +61,47 @@ export namespace mockServices {
export namespace cache {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ get: jest.MockInstance<
+ Promise,
+ [key: string],
+ unknown
+ >;
+ set: jest.MockInstance<
+ Promise,
+ [
+ key: string,
+ value: JsonValue,
+ options?: CacheServiceSetOptions | undefined,
+ ],
+ unknown
+ >;
+ delete: jest.MockInstance, [key: string], unknown>;
+ withOptions: jest.MockInstance<
+ CacheService,
+ [options: CacheServiceOptions],
+ unknown
+ >;
+ } & CacheService;
}
// (undocumented)
export namespace database {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ getClient: jest.MockInstance>, [], unknown>;
+ migrations?:
+ | {
+ skip?: boolean | undefined;
+ }
+ | undefined;
+ } & DatabaseService;
}
// (undocumented)
export namespace httpRouter {
@@ -45,6 +109,12 @@ export namespace mockServices {
factory: (
options?: HttpRouterFactoryOptions | undefined,
) => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ use: jest.MockInstance;
+ } & HttpRouterService;
}
// (undocumented)
export function identity(): IdentityService;
@@ -52,21 +122,99 @@ export namespace mockServices {
export namespace identity {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ getIdentity: jest.MockInstance<
+ Promise,
+ [options: IdentityApiGetIdentityRequest],
+ unknown
+ >;
+ } & IdentityService;
}
// (undocumented)
export namespace lifecycle {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ addStartupHook: jest.MockInstance<
+ void,
+ [
+ hook: LifecycleServiceStartupHook,
+ options?: LifecycleServiceStartupOptions | undefined,
+ ],
+ unknown
+ >;
+ addShutdownHook: jest.MockInstance<
+ void,
+ [
+ hook: LifecycleServiceShutdownHook,
+ options?: LifecycleServiceShutdownOptions | undefined,
+ ],
+ unknown
+ >;
+ } & LifecycleService;
}
// (undocumented)
export namespace logger {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ error: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ warn: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ info: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ debug: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ child: jest.MockInstance;
+ } & LoggerService;
}
// (undocumented)
export namespace permissions {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ authorize: jest.MockInstance<
+ Promise,
+ [
+ requests: AuthorizePermissionRequest[],
+ options?: EvaluatorRequestOptions | undefined,
+ ],
+ unknown
+ >;
+ authorizeConditional: jest.MockInstance<
+ Promise,
+ [
+ requests: QueryPermissionRequest[],
+ options?: EvaluatorRequestOptions | undefined,
+ ],
+ unknown
+ >;
+ } & PermissionsService;
}
// (undocumented)
export function rootConfig(options?: rootConfig.Options): RootConfigService;
@@ -85,6 +233,27 @@ export namespace mockServices {
export namespace rootLifecycle {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ addStartupHook: jest.MockInstance<
+ void,
+ [
+ hook: LifecycleServiceStartupHook,
+ options?: LifecycleServiceStartupOptions | undefined,
+ ],
+ unknown
+ >;
+ addShutdownHook: jest.MockInstance<
+ void,
+ [
+ hook: LifecycleServiceShutdownHook,
+ options?: LifecycleServiceShutdownOptions | undefined,
+ ],
+ unknown
+ >;
+ } & RootLifecycleService;
}
// (undocumented)
export function rootLogger(options?: rootLogger.Options): LoggerService;
@@ -98,11 +267,58 @@ export namespace mockServices {
factory: (
options?: Options | undefined,
) => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ error: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ warn: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ info: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ debug: jest.MockInstance<
+ void,
+ [message: string, meta?: Error | JsonObject | undefined],
+ unknown
+ >;
+ child: jest.MockInstance;
+ } & RootLoggerService;
}
// (undocumented)
export namespace scheduler {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ triggerTask: jest.MockInstance, [id: string], unknown>;
+ scheduleTask: jest.MockInstance<
+ Promise,
+ [task: TaskScheduleDefinition & TaskInvocationDefinition],
+ unknown
+ >;
+ createScheduledTaskRunner: jest.MockInstance<
+ TaskRunner,
+ [schedule: TaskScheduleDefinition],
+ unknown
+ >;
+ getScheduledTasks: jest.MockInstance<
+ Promise,
+ [],
+ unknown
+ >;
+ } & SchedulerService;
}
// (undocumented)
export function tokenManager(): TokenManagerService;
@@ -110,11 +326,48 @@ export namespace mockServices {
export namespace tokenManager {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ getToken: jest.MockInstance<
+ Promise<{
+ token: string;
+ }>,
+ [],
+ unknown
+ >;
+ authenticate: jest.MockInstance<
+ Promise,
+ [token: string],
+ unknown
+ >;
+ } & TokenManagerService;
}
// (undocumented)
export namespace urlReader {
const // (undocumented)
factory: () => ServiceFactory;
+ const // (undocumented)
+ mock: (partialImpl?: Partial | undefined) => {
+ factory: ServiceFactory;
+ } & {
+ readUrl: jest.MockInstance<
+ Promise,
+ [url: string, options?: ReadUrlOptions | undefined],
+ unknown
+ >;
+ readTree: jest.MockInstance<
+ Promise,
+ [url: string, options?: ReadTreeOptions | undefined],
+ unknown
+ >;
+ search: jest.MockInstance<
+ Promise,
+ [url: string, options?: SearchOptions | undefined],
+ unknown
+ >;
+ } & UrlReaderService;
}
}
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index 02cb1d5cf9..21946be36c 100644
--- a/packages/backend-test-utils/package.json
+++ b/packages/backend-test-utils/package.json
@@ -57,6 +57,9 @@
"testcontainers": "^8.1.2",
"uuid": "^8.0.0"
},
+ "peerDependencies": {
+ "@types/jest": "*"
+ },
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts
index 0455b9f1ae..d0bb79dd2e 100644
--- a/packages/backend-test-utils/src/next/services/mockServices.ts
+++ b/packages/backend-test-utils/src/next/services/mockServices.ts
@@ -40,6 +40,7 @@ import { JsonObject } from '@backstage/types';
import { MockIdentityService } from './MockIdentityService';
import { MockRootLoggerService } from './MockRootLoggerService';
+/** @internal */
function simpleFactory<
TService,
TScope extends 'root' | 'plugin',
@@ -57,6 +58,34 @@ function simpleFactory<
})) as (...options: TOptions) => ServiceFactory;
}
+/** @internal */
+function simpleMock(
+ ref: ServiceRef,
+ mockFactory: () => jest.Mocked,
+): (
+ partialImpl?: Partial,
+) => { factory: ServiceFactory } & jest.Mocked {
+ 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,
+ })(),
+ });
+ };
+}
+
/**
* @public
*/
@@ -79,6 +108,13 @@ export namespace mockServices {
};
export const factory = simpleFactory(coreServices.rootLogger, rootLogger);
+ export const mock = simpleMock(coreServices.rootLogger, () => ({
+ child: jest.fn(),
+ debug: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ }));
}
export function tokenManager(): TokenManagerService {
@@ -98,6 +134,10 @@ export namespace mockServices {
coreServices.tokenManager,
tokenManager,
);
+ export const mock = simpleMock(coreServices.tokenManager, () => ({
+ authenticate: jest.fn(),
+ getToken: jest.fn(),
+ }));
}
export function identity(): IdentityService {
@@ -105,6 +145,9 @@ export namespace mockServices {
}
export namespace identity {
export const factory = simpleFactory(coreServices.identity, identity);
+ export const mock = simpleMock(coreServices.identity, () => ({
+ getIdentity: jest.fn(),
+ }));
}
// TODO(Rugvip): Not all core services have implementations available here yet.
@@ -112,29 +155,71 @@ export namespace mockServices {
// re-implement functioning mock versions here.
export namespace cache {
export const factory = cacheServiceFactory;
+ export const mock = simpleMock(coreServices.cache, () => ({
+ delete: jest.fn(),
+ get: jest.fn(),
+ set: jest.fn(),
+ withOptions: jest.fn(),
+ }));
}
export namespace database {
export const factory = databaseServiceFactory;
+ export const mock = simpleMock(coreServices.database, () => ({
+ getClient: jest.fn(),
+ }));
}
export namespace httpRouter {
export const factory = httpRouterServiceFactory;
+ export const mock = simpleMock(coreServices.httpRouter, () => ({
+ use: jest.fn(),
+ }));
}
export namespace lifecycle {
export const factory = lifecycleServiceFactory;
+ export const mock = simpleMock(coreServices.lifecycle, () => ({
+ addShutdownHook: jest.fn(),
+ addStartupHook: jest.fn(),
+ }));
}
export namespace logger {
export const factory = loggerServiceFactory;
+ export const mock = simpleMock(coreServices.logger, () => ({
+ child: jest.fn(),
+ debug: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ }));
}
export namespace permissions {
export const factory = permissionsServiceFactory;
+ export const mock = simpleMock(coreServices.permissions, () => ({
+ authorize: jest.fn(),
+ authorizeConditional: jest.fn(),
+ }));
}
export namespace rootLifecycle {
export const factory = rootLifecycleServiceFactory;
+ export const mock = simpleMock(coreServices.rootLifecycle, () => ({
+ addShutdownHook: jest.fn(),
+ addStartupHook: jest.fn(),
+ }));
}
export namespace scheduler {
export const factory = schedulerServiceFactory;
+ export const mock = simpleMock(coreServices.scheduler, () => ({
+ createScheduledTaskRunner: jest.fn(),
+ getScheduledTasks: jest.fn(),
+ scheduleTask: jest.fn(),
+ triggerTask: jest.fn(),
+ }));
}
export namespace urlReader {
export const factory = urlReaderServiceFactory;
+ export const mock = simpleMock(coreServices.urlReader, () => ({
+ readTree: jest.fn(),
+ readUrl: jest.fn(),
+ search: jest.fn(),
+ }));
}
}
diff --git a/yarn.lock b/yarn.lock
index ec0379375a..fd3d589b8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3514,6 +3514,8 @@ __metadata:
supertest: ^6.1.3
testcontainers: ^8.1.2
uuid: ^8.0.0
+ peerDependencies:
+ "@types/jest": "*"
languageName: unknown
linkType: soft