diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index 9c6db0d3de..82f44df2eb 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -31,7 +31,7 @@ describe('Entity details component', () => { }); ``` -To mock [Utility APIs](../architecture/33-utility-apis.md) that are used by your component, pass API overrides to `renderInTestApp` using the `apis` option. Mock helpers are available from `@backstage/frontend-test-utils` and plugin-specific test utilities: +To mock [Utility APIs](../architecture/33-utility-apis.md) that are used by your component, pass API overrides to `renderInTestApp` using the `apis` option. Mock helpers are available from `@backstage/frontend-test-utils` and plugin-specific test utilities. For a deeper look at the available mock APIs and how to create your own, see [Testing with Utility APIs](../utility-apis/05-testing.md). ```tsx import { screen } from '@testing-library/react'; diff --git a/docs/frontend-system/utility-apis/01-index.md b/docs/frontend-system/utility-apis/01-index.md index 57e895ca36..86d317ddb2 100644 --- a/docs/frontend-system/utility-apis/01-index.md +++ b/docs/frontend-system/utility-apis/01-index.md @@ -35,6 +35,14 @@ Most utility APIs are usable directly without any configuration. But they are pr These cases are all described in [the main article](./04-configuring.md). +## Testing with utility APIs + +> For details, [see the main article](./05-testing.md). + +When testing frontend components and extensions, you often need to provide mock implementations of the utility APIs they depend on. The `@backstage/frontend-test-utils` package provides the `mockApis` namespace with ready-made mocks for all core utility APIs, which can be passed to test utilities like `renderInTestApp` and `TestApiProvider`. + +These are described in detail in [the main article](./05-testing.md). + ## Migrating from the old frontend system If you want to learn how to migrate your own utility APIs from the old frontend system to the new one, that's described in the [Migrating APIs guide](../building-plugins/05-migrating.md#migrating-apis). diff --git a/docs/frontend-system/utility-apis/05-testing.md b/docs/frontend-system/utility-apis/05-testing.md new file mode 100644 index 0000000000..f1a9110590 --- /dev/null +++ b/docs/frontend-system/utility-apis/05-testing.md @@ -0,0 +1,172 @@ +--- +id: testing +title: Testing with Utility APIs +sidebar_label: Testing +description: Mocking and testing Utility APIs +--- + +When testing frontend components and extensions, you often need to provide mock implementations of the utility APIs they depend on. The `@backstage/frontend-test-utils` package provides the `mockApis` namespace with ready-made mocks for all core utility APIs. + +## The `mockApis` namespace + +The `mockApis` namespace is the main entry point for creating mock utility API instances in tests. It provides three usage patterns for each API: + +### Fake instances + +Call the API function directly to create a fake instance with simplified but functional behavior. These are useful when your test needs the API to actually work, not just be stubbed. + +```ts +import { mockApis } from '@backstage/frontend-test-utils'; + +const configApi = mockApis.config({ data: { app: { title: 'Test' } } }); +configApi.getString('app.title'); // 'Test' + +const alertApi = mockApis.alert(); +alertApi.post({ message: 'hello' }); +expect(alertApi.getAlerts()).toHaveLength(1); +``` + +### Jest mocks + +Call `.mock()` to get an instance where every method is a `jest.fn()`. You can optionally provide partial implementations. This is useful when you want to assert that specific methods were called. + +```ts +import { mockApis } from '@backstage/frontend-test-utils'; + +const catalogApi = mockApis.permission.mock({ + authorize: async () => ({ result: AuthorizeResult.ALLOW }), +}); + +// ... exercise the component ... + +expect(catalogApi.authorize).toHaveBeenCalledTimes(1); +``` + +### API factories + +Call `.factory()` to get an `ApiFactory` suitable for more advanced wiring scenarios. + +## Providing mock APIs in tests + +### With `renderInTestApp` + +```tsx +import { screen } from '@testing-library/react'; +import { renderInTestApp, mockApis } from '@backstage/frontend-test-utils'; + +await renderInTestApp(, { + apis: [ + mockApis.identity({ userEntityRef: 'user:default/guest' }), + mockApis.config({ data: { app: { title: 'Test App' } } }), + ], +}); +``` + +You can also use the `[apiRef, implementation]` tuple syntax to provide any API implementation, including ones that aren't from `mockApis`: + +```tsx +import { myCustomApiRef } from '../apis'; + +const myCustomApiInstance = { + // ... +}; + +await renderInTestApp(, { + apis: [ + mockApis.identity({ userEntityRef: 'user:default/guest' }), + [myCustomApiRef, myCustomApiInstance], + ], +}); +``` + +### With `renderTestApp` + +The same `apis` option is available on `renderTestApp`, which is commonly used when testing extensions or entity pages: + +```tsx +import { renderTestApp, mockApis } from '@backstage/frontend-test-utils'; +import { createTestEntityPage } from '@backstage/plugin-catalog-react/testUtils'; + +renderTestApp({ + extensions: [createTestEntityPage({ entity }), myEntityCard], + apis: [mockApis.permission()], +}); +``` + +### With `TestApiProvider` + +For standalone rendering scenarios where you're not using `renderInTestApp`, the `TestApiProvider` component accepts the same `apis` format: + +```tsx +import { render } from '@testing-library/react'; +import { TestApiProvider, mockApis } from '@backstage/frontend-test-utils'; + +render( + + + , +); +``` + +## Plugin-specific test mocks + +Plugins can provide their own mock APIs that follow the same pattern. For example, `@backstage/plugin-catalog-react` provides `catalogApiMock` in its `/testUtils` entry point: + +```tsx +import { renderTestApp } from '@backstage/frontend-test-utils'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; + +renderTestApp({ + extensions: [myExtension], + apis: [catalogApiMock({ entities: [entity] })], +}); +``` + +### Creating your own mock APIs with `attachMockApiFactory` + +If you maintain a plugin that exposes a utility API, you can use `attachMockApiFactory` to create mock instances that can be passed directly to test utilities: + +```ts +import { attachMockApiFactory } from '@backstage/frontend-test-utils'; +import { myApiRef, type MyApi } from '@internal/plugin-example-react'; + +export function myApiMock(options?: { greeting?: string }): MyApi { + const instance: MyApi = { + greet: async () => options?.greeting ?? 'Hello!', + }; + return attachMockApiFactory(myApiRef, instance); +} +``` + +Consumers can then use it like the built-in mocks: + +```tsx +await renderInTestApp(, { + apis: [myApiMock({ greeting: 'Hi there!' })], +}); +``` + +## Available mock APIs + +The table below lists all core APIs available through the `mockApis` namespace. + +| API | Fake instance | Notes | +| --------------------------------- | --------------------- | ---------------------------------------------------------------------- | +| `mockApis.alert()` | `MockAlertApi` | Collects alerts; has `getAlerts()`, `clearAlerts()`, `waitForAlert()` | +| `mockApis.analytics()` | `MockAnalyticsApi` | Collects events; has `getEvents()` | +| `mockApis.config({ data })` | `MockConfigApi` | Reads from a plain JSON object | +| `mockApis.discovery({ baseUrl })` | Inline | Returns `${baseUrl}/api/${pluginId}`, defaults to `http://example.com` | +| `mockApis.error(options?)` | `MockErrorApi` | Collects errors; has `getErrors()`, `waitForError()` | +| `mockApis.featureFlags(options?)` | `MockFeatureFlagsApi` | In-memory flag state; has `getState()`, `setState()`, `clearState()` | +| `mockApis.fetch(options?)` | `MockFetchApi` | Wraps `cross-fetch`; supports identity injection and plugin protocol | +| `mockApis.identity(options?)` | Inline | Configurable user ref, ownership, token, profile | +| `mockApis.permission(options?)` | `MockPermissionApi` | Defaults to `ALLOW`; accepts a handler function | +| `mockApis.storage({ data })` | `MockStorageApi` | In-memory storage with bucket support | +| `mockApis.translation()` | `MockTranslationApi` | Passthrough returning default messages from translation refs | + +Each of these also has a `.mock()` variant that returns jest mocks, as described above. diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index b5955702a9..1e2da6ff2b 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -599,6 +599,7 @@ export default { 'frontend-system/utility-apis/creating', 'frontend-system/utility-apis/consuming', 'frontend-system/utility-apis/configuring', + 'frontend-system/utility-apis/testing', ], ), ],