Skip to content

Commit

Permalink
Merge pull request #24873 from backstage/freben/cache-test
Browse files Browse the repository at this point in the history
backend-test-utils: add cache testing utilities
  • Loading branch information
freben committed May 23, 2024
2 parents 3b28632 + 805cbe7 commit 024b530
Show file tree
Hide file tree
Showing 18 changed files with 678 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-jobs-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': minor
---

Added `TestCaches` that functions just like `TestDatabases`
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ jobs:
ports:
- 3306/tcp
redis:
image: redis
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
Expand Down Expand Up @@ -240,7 +240,7 @@ jobs:
BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
BACKSTAGE_TEST_CACHE_REDIS_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }}
BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }}

# We run the test cases before verifying the specs to prevent any failing tests from causing errors.
- name: verify openapi specs against test cases
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/deploy_packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ jobs:
--health-retries 5
ports:
- 3306/tcp
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379/tcp

env:
CI: true
Expand Down Expand Up @@ -115,6 +124,7 @@ jobs:
BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }}

- name: Discord notification
if: ${{ failure() }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,70 @@
* limitations under the License.
*/

import { mockServices } from '@backstage/backend-test-utils';
import { mockServices, TestCaches } from '@backstage/backend-test-utils';
import KeyvRedis from '@keyv/redis';
import KeyvMemcache from '@keyv/memcache';
import { CacheManager } from './CacheManager';

// This test is in a separate file because the main test file uses other mocking
// that might interfere with this one.

// Contrived code because it's hard to spy on a default export
jest.mock('@keyv/redis', () => {
const ActualKeyvRedis = jest.requireActual('@keyv/redis');
const Actual = jest.requireActual('@keyv/redis');
return jest.fn((...args: any[]) => {
return new ActualKeyvRedis(...args);
return new Actual(...args);
});
});
jest.mock('@keyv/memcache', () => {
const Actual = jest.requireActual('@keyv/memcache');
return jest.fn((...args: any[]) => {
return new Actual(...args);
});
});

describe('CacheManager integration', () => {
describe('redis', () => {
it('only creates one underlying connection', async () => {
const connection =
process.env.BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING;
if (!connection) {
return;
}
const caches = TestCaches.create();

afterEach(jest.clearAllMocks);

it.each(caches.eachSupportedId())(
'only creates one underlying connection, %p',
async cacheId => {
const { store, connection } = await caches.init(cacheId);

const manager = CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: { cache: { store: 'redis', connection } },
},
data: { backend: { cache: { store, connection } } },
}),
{ onError: e => expect(e).not.toBeDefined() },
);

manager.forPlugin('p1').getClient();
manager.forPlugin('p1').getClient({ defaultTtl: 200 });
manager.forPlugin('p2').getClient();
manager.forPlugin('p3').getClient({});

expect(KeyvRedis).toHaveBeenCalledTimes(1);
});

it('interacts correctly with redis', async () => {
// TODO(freben): This could be frameworkified as TestCaches just like
// TestDatabases, but that will have to come some other day
const connection =
process.env.BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING;
if (!connection) {
return;
if (store === 'redis') {
// eslint-disable-next-line jest/no-conditional-expect
expect(KeyvRedis).toHaveBeenCalledTimes(1);
} else if (store === 'memcache') {
// eslint-disable-next-line jest/no-conditional-expect
expect(KeyvMemcache).toHaveBeenCalledTimes(1);
}
},
);

it.each(caches.eachSupportedId())(
'interacts correctly with store, %p',
async cacheId => {
const { store, connection } = await caches.init(cacheId);

const manager = CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: { cache: { store: 'redis', connection } },
backend: { cache: { store, connection } },
},
}),
{ onError: e => expect(e).not.toBeDefined() },
);

const plugin1 = manager.forPlugin('p1').getClient();
Expand All @@ -84,6 +91,6 @@ describe('CacheManager integration', () => {
await expect(plugin1.get('a')).resolves.toBe('plugin1');
await expect(plugin2a.get('a')).resolves.toBe('plugin2b');
await expect(plugin2b.get('a')).resolves.toBe('plugin2b');
});
});
},
);
});
23 changes: 23 additions & 0 deletions packages/backend-test-utils/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { HttpRouterFactoryOptions } from '@backstage/backend-app-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
import { IdentityService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import Keyv from 'keyv';
import { Knex } from 'knex';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
Expand Down Expand Up @@ -425,6 +426,28 @@ export interface TestBackendOptions<TExtensionPoints extends any[]> {
>;
}

// @public
export type TestCacheId = 'MEMORY' | 'REDIS_7' | 'MEMCACHED_1';

// @public
export class TestCaches {
static create(options?: {
ids?: TestCacheId[];
disableDocker?: boolean;
}): TestCaches;
// (undocumented)
eachSupportedId(): [TestCacheId][];
init(id: TestCacheId): Promise<{
store: string;
connection: string;
keyv: Keyv;
}>;
// (undocumented)
static setDefaults(options: { ids?: TestCacheId[] }): void;
// (undocumented)
supports(id: TestCacheId): boolean;
}

// @public
export type TestDatabaseId =
| 'POSTGRES_16'
Expand Down
4 changes: 4 additions & 0 deletions packages/backend-test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/types": "workspace:^",
"@keyv/memcache": "^1.3.5",
"@keyv/redis": "^2.5.3",
"@types/keyv": "^4.2.0",
"better-sqlite3": "^9.0.0",
"cookie": "^0.6.0",
"express": "^4.17.1",
"fs-extra": "^11.0.0",
"keyv": "^4.5.2",
"knex": "^3.0.0",
"msw": "^1.0.0",
"mysql2": "^3.0.0",
Expand Down
53 changes: 53 additions & 0 deletions packages/backend-test-utils/src/cache/TestCaches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 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 { isDockerDisabledForTests } from '../util';
import { TestCaches } from './TestCaches';

const itIfDocker = isDockerDisabledForTests() ? it.skip : it;

jest.setTimeout(60_000);

describe('TestCaches', () => {
const caches = TestCaches.create();

it.each(caches.eachSupportedId())('fires up a cache, %p', async cacheId => {
const { keyv } = await caches.init(cacheId);
await keyv.set('test', 'value');
await expect(keyv.get('test')).resolves.toBe('value');
});

itIfDocker('clears between tests, part 1', async () => {
const { keyv } = await caches.init('REDIS_7');
// eslint-disable-next-line jest/no-standalone-expect
await expect(keyv.get('collision')).resolves.toBeUndefined();
await keyv.set('collision', 'something');
});

itIfDocker('clears between tests, part 2', async () => {
const { keyv } = await caches.init('REDIS_7');
// eslint-disable-next-line jest/no-standalone-expect
await expect(keyv.get('collision')).resolves.toBeUndefined();
await keyv.set('collision', 'something');
});

itIfDocker('clears between tests, part 3', async () => {
const { keyv } = await caches.init('REDIS_7');
// eslint-disable-next-line jest/no-standalone-expect
await expect(keyv.get('collision')).resolves.toBeUndefined();
await keyv.set('collision', 'something');
});
});
Loading

0 comments on commit 024b530

Please sign in to comment.