From ff53f764e1f4f171364eeb6b48b00d5f31b60f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 22 Jan 2023 11:33:03 +0100 Subject: [PATCH 1/3] add plugin / module testing docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../02-testing.md | 153 +++++++++++++++++- 1 file changed, 147 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index e8a8619f72..9a2042c4d3 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -6,21 +6,162 @@ sidebar_label: Testing description: Learn how to test your backend plugins and modules --- -Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. -`startTestBackend` returns a server which can be used together with `supertest` to test the plugins. +Utilities for testing backend plugins and modules are available in +`@backstage/backend-test-utils`. This section describes those facilities. + +## Testing Backend Plugins + +To facilitate testing of backend plugins, the `@backstage/backend-test-utils` +package provides a `startTestBackend` function which starts up an entire backend +harness, complete with a number of mock services. You can then provide overrides +for services whose behavior you need to adjust for the test run. + +The function returns an HTTP server instance which can be used together with +e.g. `supertest` to easily test the actual REST service surfaces of plugins who +register routes with [the HTTP router service +API](../core-services/01-index.md). ```ts -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import request from 'supertest'; +import { myPlugin } from './plugin.ts'; + +describe('myPlugin', () => { + it('can serve values from config', async () => { + const fakeConfig = { myPlugin: { value: 7 } }; -describe('My plugin tests', () => { - it('should return 200', async () => { const { server } = await startTestBackend({ features: [myPlugin()], + services: [mockServices.config.factory({ data: fakeConfig })], }); - const response = await request(server).get('/api/example/hello'); + const response = await request(server).get('/api/example/get-value'); expect(response.status).toBe(200); + expect(response.body).toEqual({ value: 7 }); }); }); ``` + +This example shows how to easily access the factories for mock services and +passing options to them, which will override the default mocks. + +The returned server also has a `port()` method which returns the dynamically +bound listening port. You can use this to perform lower level network +interactions with the running test service. + +## Testing Remote Service Interactions + +If your backend plugin or service interacts with external services using HTTP +calls, we recommend leveraging the `msw` package to intercept actual outgoing +requests and return mock responses. This lets you stub out remote services +rather than the local clients, leading to more thorough and robust tests. You +can read more about how it works [in their documentation](https://mswjs.io/). + +The `@backstage/backend-test-utils` package exports a `setupRequestMockHandlers` +function which ensures that the correct `jest` lifecycle hooks are invoked to +set up and tear down your `msw` instance, and enables the option that completely +rejects requests that don't match one of your mock rules. This ensures that your +tests cannot accidentally leak traffic into production from tests. + +Example: + +```ts +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +describe('read from remote', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('should auth and read successfully', async () => { + expect.assertions(1); + + worker.use( + rest.get('https://remote-server.com/api/v3/foo', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe('Bearer fake'); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.body(JSON.stringify({ value: 7 })), + ); + }), + ); + + // exercise your plugin or service as usual, with real clients + }); +}); +``` + +## Testing Database Interactions + +The `@backstage/backend-test-utils` package includes facilities for testing your +plugins' interactions with databases, including spinning up `testcontainers` +powered Docker images with real database engines to connect to. + +The base setup for such a test could look as follows: + +```ts +// MyDatabaseClass.test.ts +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + MyDatabaseClass, + applyDatabaseMigrations, + type FooTableRow, +} from './MyDatabaseClass'; + +describe('MyDatabaseClass', () => { + // Change this to the set of constants that you actually actively intend to + // support. Make sure to create only one TestDatabases instance per file, + // since spinning up "physical" databases to test against is much costlier + // than creating the "logical" databases within them that the individual + // tests use. + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + // Just an example of how to conveniently bundle up the setup code + async function createSut(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + const sut = new MyDatabaseClass({ database: knex }); + await sut.runMigrations(); + return { knex, sut }; + } + + describe('foo', () => { + // Easily run the exact same test onto all supported databases + it.each(databases.eachSupportedId())( + 'should run foo on %p', + async databaseId => { + const { knex, sut } = await createSut(databaseId); + // raw knex is available for underlying manipulation + await knex('foo').insert({ value: 2 }); + // drive your system under test as usual + await expect(sut.foos()).resolves.toEqual([{ value: 2 }]); + }); + }); +``` + +If you want to pass the test database instance into backend plugins or services, +you can supply it in the form of a mock instance of `coreServices.database` to +your test database. + +```ts +const { knex, sut } = await createSut(databaseId); +const { server } = await startTestBackend({ + features: [myPlugin()], + services: [[coreServices.database, { getClient: async () => knex }]], +}); +``` + +When running locally, the tests only run against SQLite for the sake of speed. +When the `CI` environment variable is set, all given database engines are used. + +If you do not want or are unable to use docker based database engines, e.g. if +your CI environment is able to supply databases natively, the `TestDatabases` +support custom connection strings through the use of environment variables that +it'll take into account when present. + +- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING` From 69df5e168ce35ac702e9ab0659304d68b6a353fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 14:23:41 +0100 Subject: [PATCH 2/3] Update docs/backend-system/building-plugins-and-modules/02-testing.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Signed-off-by: Fredrik Adelöw --- .../backend-system/building-plugins-and-modules/02-testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 9a2042c4d3..70685e0664 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -42,8 +42,8 @@ describe('myPlugin', () => { }); ``` -This example shows how to easily access the factories for mock services and -passing options to them, which will override the default mocks. +This example shows how to access the mock service factories and +pass options to them, which will override the default mock services. The returned server also has a `port()` method which returns the dynamically bound listening port. You can use this to perform lower level network From 965b91b97b9bff3cd4dd88b854e6e4d033339e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Jan 2023 14:34:19 +0100 Subject: [PATCH 3/3] address comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../02-testing.md | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 70685e0664..3b19a7cfc8 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -9,12 +9,15 @@ description: Learn how to test your backend plugins and modules Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. This section describes those facilities. -## Testing Backend Plugins +## Testing Backend Plugins and Modules -To facilitate testing of backend plugins, the `@backstage/backend-test-utils` -package provides a `startTestBackend` function which starts up an entire backend -harness, complete with a number of mock services. You can then provide overrides -for services whose behavior you need to adjust for the test run. +To facilitate testing of backend plugins and modules, the +`@backstage/backend-test-utils` package provides a `startTestBackend` function +which starts up an entire backend harness, complete with a number of mock +services. You can then provide overrides for services whose behavior you need to +adjust for the test run. The function also accepts a number of _features_ (a +collective term for backend [plugins](../architecture/04-plugins.md) and +[modules](../architecture/06-modules.md)), that are the subjects of the test. The function returns an HTTP server instance which can be used together with e.g. `supertest` to easily test the actual REST service surfaces of plugins who @@ -112,20 +115,20 @@ import { describe('MyDatabaseClass', () => { // Change this to the set of constants that you actually actively intend to - // support. Make sure to create only one TestDatabases instance per file, - // since spinning up "physical" databases to test against is much costlier - // than creating the "logical" databases within them that the individual - // tests use. + // support. This create call must be made inside a describe block. Make sure + // to create only one TestDatabases instance per file, since spinning up + // "physical" databases to test against is much costlier than creating the + // "logical" databases within them that the individual tests use. const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); // Just an example of how to conveniently bundle up the setup code - async function createSut(databaseId: TestDatabaseId) { + async function createSubject(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); - const sut = new MyDatabaseClass({ database: knex }); - await sut.runMigrations(); - return { knex, sut }; + const subject = new MyDatabaseClass({ database: knex }); + await subject.runMigrations(); + return { knex, subject }; } describe('foo', () => { @@ -133,11 +136,11 @@ describe('MyDatabaseClass', () => { it.each(databases.eachSupportedId())( 'should run foo on %p', async databaseId => { - const { knex, sut } = await createSut(databaseId); + const { knex, subject } = await createSubject(databaseId); // raw knex is available for underlying manipulation await knex('foo').insert({ value: 2 }); // drive your system under test as usual - await expect(sut.foos()).resolves.toEqual([{ value: 2 }]); + await expect(subject.foos()).resolves.toEqual([{ value: 2 }]); }); }); ``` @@ -147,7 +150,7 @@ you can supply it in the form of a mock instance of `coreServices.database` to your test database. ```ts -const { knex, sut } = await createSut(databaseId); +const { knex, subject } = await createSubject(databaseId); const { server } = await startTestBackend({ features: [myPlugin()], services: [[coreServices.database, { getClient: async () => knex }]],