backend-test-utils: refactor into ServiceFactoryTester

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-29 18:38:48 +02:00
parent 5b8b654bfe
commit bbff821e54
5 changed files with 133 additions and 99 deletions
+23 -13
View File
@@ -26,25 +26,13 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { RootLoggerService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
// @public (undocumented)
export function isDockerDisabledForTests(): boolean;
// @public (undocumented)
export function makeServiceFactoryTester<
TService,
TScope extends 'root' | 'plugin',
>(
subject:
| ServiceFactory<TService, TScope>
| (() => ServiceFactory<TService, TScope>),
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
): 'root' extends TScope
? () => Promise<TService>
: (pluginId: string) => Promise<TService>;
// @public (undocumented)
export namespace mockServices {
// (undocumented)
@@ -195,6 +183,28 @@ export namespace mockServices {
}
}
// @public (undocumented)
export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
// (undocumented)
static from<TService, TScope extends 'root' | 'plugin'>(
subject:
| ServiceFactory<TService, TScope>
| (() => ServiceFactory<TService, TScope>),
options?: {
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>;
},
): ServiceFactoryTester<TService, TScope>;
// (undocumented)
get(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService>;
// (undocumented)
getService<TGetService, TGetScope extends 'root' | 'plugin'>(
service: ServiceRef<TGetService, TGetScope>,
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
): Promise<TGetService>;
}
// @public (undocumented)
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
@@ -19,7 +19,7 @@ import {
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { makeServiceFactoryTester } from './makeServiceFactoryTester';
import { ServiceFactoryTester } from './ServiceFactoryTester';
const rootServiceRef = createServiceRef<string>({ id: 'a', scope: 'root' });
const pluginServiceRef = createServiceRef<string>({ id: 'b', scope: 'plugin' });
@@ -52,85 +52,85 @@ const sharedPluginFactory = createServiceFactory({
},
});
describe('makeServiceFactoryTester', () => {
describe('ServiceFactoryTester', () => {
it('should test a root service factory', async () => {
const factoryTester = makeServiceFactoryTester(rootFactory());
const tester = ServiceFactoryTester.from(rootFactory());
await expect(factoryTester()).resolves.toBe('root');
await expect(tester.get()).resolves.toBe('root');
});
it('should test a plugin service factory', async () => {
const factoryTester = makeServiceFactoryTester(pluginFactory());
const tester = ServiceFactoryTester.from(pluginFactory());
await expect(factoryTester('x')).resolves.toBe('x-plugin');
await expect(factoryTester('y')).resolves.toBe('y-plugin');
await expect(factoryTester('z')).resolves.toBe('z-plugin');
await expect(tester.get('x')).resolves.toBe('x-plugin');
await expect(tester.get('y')).resolves.toBe('y-plugin');
await expect(tester.get('z')).resolves.toBe('z-plugin');
});
it('should test a plugin service factory with root context', async () => {
const factoryTester = makeServiceFactoryTester(sharedPluginFactory());
const tester = ServiceFactoryTester.from(sharedPluginFactory());
await expect(factoryTester('x')).resolves.toBe('x-1-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin');
await expect(factoryTester('z')).resolves.toBe('z-3-plugin');
await expect(tester.get('x')).resolves.toBe('x-1-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin');
await expect(tester.get('z')).resolves.toBe('z-3-plugin');
const tester2 = makeServiceFactoryTester(sharedPluginFactory());
const tester2 = ServiceFactoryTester.from(sharedPluginFactory());
await expect(tester2('z')).resolves.toBe('z-1-plugin');
await expect(tester2('y')).resolves.toBe('y-2-plugin');
await expect(tester2('x')).resolves.toBe('x-3-plugin');
await expect(tester2('x')).resolves.toBe('x-3-plugin');
await expect(tester2('y')).resolves.toBe('y-2-plugin');
await expect(tester2('z')).resolves.toBe('z-1-plugin');
await expect(tester2.get('z')).resolves.toBe('z-1-plugin');
await expect(tester2.get('y')).resolves.toBe('y-2-plugin');
await expect(tester2.get('x')).resolves.toBe('x-3-plugin');
await expect(tester2.get('x')).resolves.toBe('x-3-plugin');
await expect(tester2.get('y')).resolves.toBe('y-2-plugin');
await expect(tester2.get('z')).resolves.toBe('z-1-plugin');
});
it('should use dependencies', async () => {
const factoryTester = makeServiceFactoryTester(
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<string>({ id: 'concat' }),
deps: { root: rootServiceRef, plugin: pluginServiceRef },
factory: async ({ root, plugin }) => `${root}, ${plugin}`,
}),
[rootFactory, pluginFactory()],
{ dependencies: [rootFactory, pluginFactory()] },
);
await expect(factoryTester('x')).resolves.toBe('root, x-plugin');
await expect(tester.get('x')).resolves.toBe('root, x-plugin');
});
it('should use dependencies with root context', async () => {
const factoryTester = makeServiceFactoryTester(
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: createServiceRef<string>({ id: 'concat' }),
deps: { shared: sharedPluginServiceRef, plugin: pluginServiceRef },
factory: async ({ shared, plugin }) => `${shared}, ${plugin}`,
}),
[sharedPluginFactory(), pluginFactory],
{ dependencies: [sharedPluginFactory(), pluginFactory] },
);
await expect(factoryTester('x')).resolves.toBe('x-1-plugin, x-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(factoryTester('z')).resolves.toBe('z-3-plugin, z-plugin');
await expect(tester.get('x')).resolves.toBe('x-1-plugin, x-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin');
await expect(tester.get('z')).resolves.toBe('z-3-plugin, z-plugin');
});
it('should prioritize the subject implementation', async () => {
const factoryTester = makeServiceFactoryTester(
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: rootServiceRef,
deps: {},
factory: async () => 'other-root',
}),
[rootFactory],
{ dependencies: [rootFactory] },
);
await expect(factoryTester()).resolves.toBe('other-root');
await expect(tester.get()).resolves.toBe('other-root');
});
it('should throw on missing dependencies', async () => {
const factoryTester = makeServiceFactoryTester(
const tester = ServiceFactoryTester.from(
createServiceFactory({
service: pluginServiceRef,
deps: { root: rootServiceRef },
@@ -138,7 +138,7 @@ describe('makeServiceFactoryTester', () => {
}),
);
await expect(factoryTester('x')).rejects.toThrow(
await expect(tester.get('x')).rejects.toThrow(
"Failed to instantiate service 'b' for 'x' because the following dependent services are missing: 'a'",
);
});
@@ -0,0 +1,73 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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.
*/
import { ServiceFactory, ServiceRef } from '@backstage/backend-plugin-api';
import { defaultServiceFactories } from './TestBackend';
// Direct internal import to avoid duplication.
// This is a relative import in order to make sure that the implementation is duplicated
// rather than leading to an import from @backstage/backend-app-api.
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry';
/** @public */
export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
readonly #subject: ServiceRef<TService, TScope>;
readonly #registry: ServiceRegistry;
static from<TService, TScope extends 'root' | 'plugin'>(
subject:
| ServiceFactory<TService, TScope>
| (() => ServiceFactory<TService, TScope>),
options?: { dependencies?: Array<ServiceFactory | (() => ServiceFactory)> },
) {
return new ServiceFactoryTester(
typeof subject === 'function' ? subject() : subject,
options?.dependencies,
);
}
private constructor(
subject: ServiceFactory<TService, TScope>,
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
) {
this.#subject = subject.service;
this.#registry = new ServiceRegistry([
...defaultServiceFactories,
...(dependencies?.map(f => (typeof f === 'function' ? f() : f)) ?? []),
subject,
]);
}
async get(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService> {
const [pluginId] = args;
return this.#registry.get(this.#subject, pluginId ?? 'test')!;
}
async getService<TGetService, TGetScope extends 'root' | 'plugin'>(
service: ServiceRef<TGetService, TGetScope>,
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
): Promise<TGetService> {
const [pluginId] = args;
const instance = await this.#registry.get(service, pluginId ?? 'test');
if (instance === undefined) {
throw new Error(`Service '${service.id}' not found`);
}
return instance;
}
}
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { makeServiceFactoryTester } from './makeServiceFactoryTester';
export { ServiceFactoryTester } from './ServiceFactoryTester';
export { startTestBackend } from './TestBackend';
export type { TestBackend, TestBackendOptions } from './TestBackend';
@@ -1,49 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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.
*/
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { defaultServiceFactories } from './TestBackend';
// Direct internal import to avoid duplication.
// This is a relative import in order to make sure that the implementation is duplicated
// rather than leading to an import from @backstage/backend-app-api.
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry';
/** @public */
export function makeServiceFactoryTester<
TService,
TScope extends 'root' | 'plugin',
>(
subject:
| ServiceFactory<TService, TScope>
| (() => ServiceFactory<TService, TScope>),
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
): 'root' extends TScope
? () => Promise<TService>
: (pluginId: string) => Promise<TService> {
const subjectFactory = typeof subject === 'function' ? subject() : subject;
const services = [
...defaultServiceFactories,
...(dependencies?.map(f => (typeof f === 'function' ? f() : f)) ?? []),
subjectFactory,
];
const registry = new ServiceRegistry(services);
return async (pluginId?: string) => {
return registry.get(subjectFactory.service, pluginId ?? '')! as TService;
};
}