Skip to content

Commit 5e9698f

Browse files
chore: Create test db from template (#9265)
## About the changes Based on the first hypothesis from #9264, I decided to find an alternative way of initializing the DB, mainly trying to run migrations only once and removing that from the actual test run. I found in [Postgres template databases](https://www.postgresql.org/docs/current/manage-ag-templatedbs.html) an interesting option in combination with jest global initializer. ### Changes on how we use DBs for testing Previously, we were relying on a single DB with multiple schemas to isolate tests, but each schema was empty and required migrations or custom DB initialization scripts. With this method, we don't need to use different schema names (apparently there's no templating for schemas), and we can use new databases. We can also eliminate custom initialization code. ### Legacy tests This method also highlighted some wrong assumptions in existing tests. One example is the existence of `default` environment, that because of being deprecated is no longer available, but because tests are creating the expected db state manually, they were not updated to match the existing db state. To keep tests running green, I've added a configuration to use the `legacy` test setup (24 tests). By migrating these, we'll speed up tests, but the code of these tests has to be modified, so I leave this for another PR. ## Downsides 1. The template db initialization happens at the beginning of any test, so local development may suffer from slower unit tests. As a workaround we could define an environment variable to disable the db migration 2. Proliferation of test dbs. In ephemeral environments, this is not a problem, but for local development we should clean up from time to time. There's the possibility of cleaning up test dbs using the db name as a pattern: https://github.com/Unleash/unleash/blob/2ed2e1c27418b92e815d06c351504005cf083fd0/scripts/jest-setup.ts#L13-L18 but I didn't want to add this code yet. Opinions? ## Benefits 1. It allows us migrate only once and still get the benefits of having a well known state for tests. 3. It removes some of the custom setup for tests (which in some cases ends up testing something not realistic) 4. It removes the need of testing migrations: https://github.com/Unleash/unleash/blob/main/src/test/e2e/migrator.e2e.test.ts as migrations are run at the start 5. Forces us to keep old tests up to date when we modify our database
1 parent a8ea174 commit 5e9698f

40 files changed

+253
-306
lines changed

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@
5757
"test:report": "NODE_ENV=test PORT=4243 jest --reporters=\"default\" --reporters=\"jest-junit\"",
5858
"test:docker:cleanup": "docker rm -f unleash-postgres",
5959
"test:watch": "yarn test --watch",
60-
"test:coverage": "NODE_ENV=test PORT=4243 jest --coverage --testLocationInResults --outputFile=\"coverage/report.json\" --forceExit --testTimeout=10000",
61-
"test:coverage:jest": "NODE_ENV=test PORT=4243 jest --silent --ci --json --coverage --testLocationInResults --outputFile=\"report.json\" --forceExit --testTimeout=10000",
62-
"test:updateSnapshot": "NODE_ENV=test PORT=4243 jest --updateSnapshot --testTimeout=10000",
60+
"test:coverage": "NODE_ENV=test PORT=4243 jest --coverage --testLocationInResults --outputFile=\"coverage/report.json\" --forceExit",
61+
"test:coverage:jest": "NODE_ENV=test PORT=4243 jest --silent --ci --json --coverage --testLocationInResults --outputFile=\"report.json\" --forceExit",
62+
"test:updateSnapshot": "NODE_ENV=test PORT=4243 jest --updateSnapshot",
6363
"seed:setup": "ts-node --compilerOptions '{\"strictNullChecks\": false}' src/test/e2e/seed/segment.seed.ts",
6464
"seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev",
6565
"clean": "del-cli --force dist",
@@ -81,7 +81,7 @@
8181
"automock": false,
8282
"maxWorkers": 4,
8383
"testTimeout": 20000,
84-
"globalSetup": "./scripts/jest-setup.js",
84+
"globalSetup": "./scripts/jest-setup.ts",
8585
"transform": {
8686
"^.+\\.tsx?$": [
8787
"@swc/jest"

scripts/jest-setup.js

-3
This file was deleted.

scripts/jest-setup.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Client, type ClientConfig } from 'pg';
2+
import { migrateDb } from '../src/migrator';
3+
import { getDbConfig } from '../src/test/e2e/helpers/database-config';
4+
5+
let initializationPromise: Promise<void> | null = null;
6+
const initializeTemplateDb = (db: ClientConfig): Promise<void> => {
7+
if (!initializationPromise) {
8+
initializationPromise = (async () => {
9+
const testDBTemplateName = process.env.TEST_DB_TEMPLATE_NAME;
10+
const client = new Client(db);
11+
await client.connect();
12+
console.log(`Initializing template database ${testDBTemplateName}`);
13+
// code to clean up, but only on next run, we could do it at tear down... but is it really needed?
14+
// const result = await client.query(`select datname from pg_database where datname like 'unleashtestdb_%';`)
15+
// result.rows.forEach(async (row: any) => {
16+
// console.log(`Dropping test database ${row.datname}`);
17+
// await client.query(`DROP DATABASE ${row.datname}`);
18+
// });
19+
await client.query(`DROP DATABASE IF EXISTS ${testDBTemplateName}`);
20+
await client.query(`CREATE DATABASE ${testDBTemplateName}`);
21+
await client.end();
22+
await migrateDb({
23+
db: { ...db, database: testDBTemplateName },
24+
} as any);
25+
console.log(`Template database ${testDBTemplateName} migrated`);
26+
})();
27+
}
28+
return initializationPromise;
29+
};
30+
31+
export default async function globalSetup() {
32+
process.env.TZ = 'UTC';
33+
process.env.TEST_DB_TEMPLATE_NAME = 'unleash_template_db';
34+
await initializeTemplateDb(getDbConfig());
35+
}

src/lib/features/change-request-access-service/sql-change-request-access-read-model.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test(`Should indicate change request enabled status`, async () => {
3131
// change request enabled in enabled environment
3232
await db.rawDatabase('change_request_settings').insert({
3333
project: 'default',
34-
environment: 'default',
34+
environment: 'development',
3535
required_approvals: 1,
3636
});
3737
const enabledStatus =
@@ -41,7 +41,7 @@ test(`Should indicate change request enabled status`, async () => {
4141
// change request enabled in disabled environment
4242
await db.stores.projectStore.deleteEnvironmentForProject(
4343
'default',
44-
'default',
44+
'development',
4545
);
4646
const disabledStatus =
4747
await readModel.isChangeRequestsEnabledForProject('default');

src/lib/features/dependent-features/dependent.features.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ let db: ITestDb;
1919
let eventStore: IEventStore;
2020

2121
beforeAll(async () => {
22-
db = await dbInit('dependent_features', getLogger);
22+
db = await dbInit('dependent_features', getLogger, {
23+
dbInitMethod: 'legacy' as const,
24+
});
2325
app = await setupAppWithCustomConfig(
2426
db.stores,
2527
{

src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ let eventBus: EventEmitter;
2727
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
2828

2929
beforeAll(async () => {
30-
db = await dbInit('feature_lifecycle', getLogger);
30+
db = await dbInit('feature_lifecycle', getLogger, {
31+
dbInitMethod: 'legacy' as const,
32+
});
3133
app = await setupAppWithAuth(
3234
db.stores,
3335
{

src/lib/features/feature-search/feature.search.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ let db: ITestDb;
1919
let stores: IUnleashStores;
2020

2121
beforeAll(async () => {
22-
db = await dbInit('feature_search', getLogger);
22+
db = await dbInit('feature_search', getLogger, {
23+
dbInitMethod: 'legacy' as const,
24+
});
2325
app = await setupAppWithAuth(
2426
db.stores,
2527
{

src/lib/features/feature-toggle/tests/archive-feature-toggles.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ let app: IUnleashTest;
1111
let db: ITestDb;
1212

1313
beforeAll(async () => {
14-
db = await dbInit('archive_test_serial', getLogger);
14+
db = await dbInit('archive_test_serial', getLogger, {
15+
dbInitMethod: 'legacy' as const,
16+
});
1517
app = await setupAppWithCustomConfig(
1618
db.stores,
1719
{

src/lib/features/feature-toggle/tests/feature-toggle-last-seen-at.e2e.test.ts

+6-49
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import {
77
setupAppWithCustomConfig,
88
} from '../../../../test/e2e/helpers/test-helper';
99
import getLogger from '../../../../test/fixtures/no-logger';
10+
import type { IUnleashOptions } from '../../../internals';
1011

1112
let app: IUnleashTest;
1213
let db: ITestDb;
1314

1415
const setupLastSeenAtTest = async (featureName: string) => {
1516
await app.createFeature(featureName);
1617

17-
await insertLastSeenAt(featureName, db.rawDatabase, 'default');
1818
await insertLastSeenAt(featureName, db.rawDatabase, 'development');
1919
await insertLastSeenAt(featureName, db.rawDatabase, 'production');
2020
};
2121

2222
beforeAll(async () => {
23-
const config = {
23+
const config: Partial<IUnleashOptions> = {
2424
experimental: {
2525
flags: {
2626
strictSchemaValidation: true,
@@ -34,29 +34,6 @@ beforeAll(async () => {
3434
config,
3535
);
3636
app = await setupAppWithCustomConfig(db.stores, config, db.rawDatabase);
37-
38-
await db.stores.environmentStore.create({
39-
name: 'development',
40-
type: 'development',
41-
sortOrder: 1,
42-
enabled: true,
43-
});
44-
45-
await db.stores.environmentStore.create({
46-
name: 'production',
47-
type: 'production',
48-
sortOrder: 2,
49-
enabled: true,
50-
});
51-
52-
await app.services.projectService.addEnvironmentToProject(
53-
'default',
54-
'development',
55-
);
56-
await app.services.projectService.addEnvironmentToProject(
57-
'default',
58-
'production',
59-
);
6037
});
6138

6239
afterAll(async () => {
@@ -67,7 +44,7 @@ afterAll(async () => {
6744
test('should return last seen at per env for /api/admin/features', async () => {
6845
await app.createFeature('lastSeenAtPerEnv');
6946

70-
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'default');
47+
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'development');
7148

7249
const response = await app.request
7350
.get('/api/admin/projects/default/features')
@@ -94,10 +71,7 @@ test('response should include last seen at per environment for multiple environm
9471

9572
const featureEnvironments = body.features[1].environments;
9673

97-
const [def, development, production] = featureEnvironments;
98-
99-
expect(def.name).toBe('default');
100-
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
74+
const [development, production] = featureEnvironments;
10175

10276
expect(development.name).toBe('development');
10377
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
@@ -117,10 +91,7 @@ test('response should include last seen at per environment for multiple environm
11791
const { body } = await app.request.get(`/api/admin/archive/features`);
11892

11993
const featureEnvironments = body.features[0].environments;
120-
const [def, development, production] = featureEnvironments;
121-
122-
expect(def.name).toBe('default');
123-
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
94+
const [development, production] = featureEnvironments;
12495

12596
expect(development.name).toBe('development');
12697
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
@@ -142,10 +113,7 @@ test('response should include last seen at per environment for multiple environm
142113
);
143114

144115
const featureEnvironments = body.features[0].environments;
145-
const [def, development, production] = featureEnvironments;
146-
147-
expect(def.name).toBe('default');
148-
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
116+
const [development, production] = featureEnvironments;
149117

150118
expect(development.name).toBe('development');
151119
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
@@ -163,13 +131,6 @@ test('response should include last seen at per environment correctly for a singl
163131
await setupLastSeenAtTest(`${featureName}4`);
164132
await setupLastSeenAtTest(`${featureName}5`);
165133

166-
await insertLastSeenAt(
167-
featureName,
168-
db.rawDatabase,
169-
'default',
170-
'2023-08-01T12:30:56.000Z',
171-
);
172-
173134
await insertLastSeenAt(
174135
featureName,
175136
db.rawDatabase,
@@ -189,10 +150,6 @@ test('response should include last seen at per environment correctly for a singl
189150
.expect(200);
190151

191152
const expected = [
192-
{
193-
name: 'default',
194-
lastSeenAt: '2023-08-01T12:30:56.000Z',
195-
},
196153
{
197154
name: 'development',
198155
lastSeenAt: '2023-08-01T12:30:56.000Z',

src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ beforeAll(async () => {
5555
db = await dbInit(
5656
'feature_toggle_service_v2_service_serial',
5757
config.getLogger,
58+
{ dbInitMethod: 'legacy' as const },
5859
);
5960
unleashConfig = config;
6061
stores = db.stores;

src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ let app: IUnleashTest;
1717
let db: ITestDb;
1818

1919
beforeAll(async () => {
20-
db = await dbInit('feature_strategy_auth_api_serial', getLogger);
20+
db = await dbInit('feature_strategy_auth_api_serial', getLogger, {
21+
dbInitMethod: 'legacy' as const,
22+
});
2123
app = await setupAppWithAuth(
2224
db.stores,
2325
{

src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ const updateStrategy = async (
9292
};
9393

9494
beforeAll(async () => {
95-
db = await dbInit('feature_strategy_api_serial', getLogger);
95+
db = await dbInit('feature_strategy_api_serial', getLogger, {
96+
dbInitMethod: 'legacy' as const,
97+
});
9698
app = await setupAppWithCustomConfig(
9799
db.stores,
98100
{

src/lib/features/frontend-api/frontend-api.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ let app: IUnleashTest;
2323
let db: ITestDb;
2424
let frontendApiService: FrontendApiService;
2525
beforeAll(async () => {
26-
db = await dbInit('frontend_api', getLogger);
26+
db = await dbInit('frontend_api', getLogger, {
27+
dbInitMethod: 'legacy' as const,
28+
});
2729
app = await setupAppWithAuth(
2830
db.stores,
2931
{

src/lib/features/instance-stats/getProductionChanges.e2e.test.ts

-12
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,6 @@ const noEnvironmentEvent = (days: number) => {
4444

4545
beforeAll(async () => {
4646
db = await dbInit('product_changes_serial', getLogger);
47-
await db.rawDatabase('environments').insert({
48-
name: 'production',
49-
type: 'production',
50-
enabled: true,
51-
protected: false,
52-
});
5347
getProductionChanges = createGetProductionChanges(db.rawDatabase);
5448
});
5549

@@ -136,12 +130,6 @@ test('five events per day should be counted correctly', async () => {
136130
});
137131

138132
test('Events posted to a non production environment should not be included in count', async () => {
139-
await db.rawDatabase('environments').insert({
140-
name: 'development',
141-
type: 'development',
142-
enabled: true,
143-
protected: false,
144-
});
145133
await db.rawDatabase
146134
.table('events')
147135
.insert(mockRawEventDaysAgo(1, 'development'));

src/lib/features/metrics/instance/metrics.test.ts

-6
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,6 @@ describe('bulk metrics', () => {
357357
enableApiToken: true,
358358
},
359359
});
360-
await authed.db('environments').insert({
361-
name: 'development',
362-
sort_order: 5000,
363-
type: 'development',
364-
enabled: true,
365-
});
366360
const clientToken =
367361
await authed.services.apiTokenService.createApiTokenWithProjects({
368362
tokenName: 'bulk-metrics-test',

src/lib/features/playground/advanced-playground.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ let app: IUnleashTest;
1010
let db: ITestDb;
1111

1212
beforeAll(async () => {
13-
db = await dbInit('advanced_playground', getLogger);
13+
db = await dbInit('advanced_playground', getLogger, {
14+
dbInitMethod: 'legacy' as const,
15+
});
1416
app = await setupAppWithCustomConfig(
1517
db.stores,
1618
{

src/lib/features/project-environments/environment-service.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ let eventService: EventService;
1818

1919
beforeAll(async () => {
2020
const config = createTestConfig();
21-
db = await dbInit('environment_service_serial', config.getLogger);
21+
db = await dbInit('environment_service_serial', config.getLogger, {
22+
dbInitMethod: 'legacy' as const,
23+
});
2224
stores = db.stores;
2325
eventService = createEventsService(db.rawDatabase, config);
2426
service = new EnvironmentService(stores, config, eventService);

src/lib/features/project-environments/environments.e2e.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ let app: IUnleashTest;
1010
let db: ITestDb;
1111

1212
beforeAll(async () => {
13-
db = await dbInit('project_environments_api_serial', getLogger);
13+
db = await dbInit('project_environments_api_serial', getLogger, {
14+
dbInitMethod: 'legacy' as const,
15+
});
1416
app = await setupAppWithCustomConfig(
1517
db.stores,
1618
{

src/lib/features/project/project-service.e2e.test.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1274,7 +1274,7 @@ test('A newly created project only gets connected to enabled environments', asyn
12741274
await projectService.createProject(project, user, auditUser);
12751275
const connectedEnvs =
12761276
await db.stores.projectStore.getEnvironmentsForProject(project.id);
1277-
expect(connectedEnvs).toHaveLength(2); // default, connection_test
1277+
expect(connectedEnvs).toHaveLength(1); // connection_test
12781278
expect(
12791279
connectedEnvs.some((e) => e.environment === enabledEnv),
12801280
).toBeTruthy();
@@ -1321,7 +1321,6 @@ test('should have environments sorted in order', async () => {
13211321
await db.stores.projectStore.getEnvironmentsForProject(project.id);
13221322

13231323
expect(connectedEnvs.map((e) => e.environment)).toEqual([
1324-
'default',
13251324
first,
13261325
second,
13271326
third,
@@ -2809,13 +2808,7 @@ describe('create project with environments', () => {
28092808
disabledEnv,
28102809
];
28112810

2812-
const allEnabledEnvs = [
2813-
'QA',
2814-
'default',
2815-
'development',
2816-
'production',
2817-
'staging',
2818-
];
2811+
const allEnabledEnvs = ['QA', 'development', 'production', 'staging'];
28192812

28202813
beforeEach(async () => {
28212814
await Promise.all(

0 commit comments

Comments
 (0)