Skip to content

Commit

Permalink
[no-issue]: Convert database tests to jest (#4188)
Browse files Browse the repository at this point in the history
  • Loading branch information
kael89 authored Sep 23, 2022
1 parent 6632bdf commit 9139e02
Show file tree
Hide file tree
Showing 21 changed files with 336 additions and 402 deletions.
2 changes: 1 addition & 1 deletion packages/database/.babelrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getIgnore = api => {
// When building @tupaia/database, babel-cli compiles in advance, so we only want it to bother
// with the last 90 days of migrations, otherwise it takes too long
return [
'src/tests/**',
'src/__tests__/**',
function (filepath) {
const filepathComponents = filepath.split('/');
const filename = filepathComponents.pop();
Expand Down
7 changes: 7 additions & 0 deletions packages/database/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const baseConfig = require('../../jest.config-js.json');

module.exports = async () => ({
...baseConfig,
rootDir: '.',
setupFilesAfterEnv: ['../../jest.setup.js', './jest.setup.js'],
});
12 changes: 12 additions & 0 deletions packages/database/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { clearTestData, getTestDatabase } from './src/testUtilities';

afterAll(async () => {
const database = getTestDatabase();
await clearTestData(database);
await database.closeConnections();
});
9 changes: 3 additions & 6 deletions packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@
"migrate-create": "scripts/migrateCreate.sh",
"migrate-down": "babel-node ./src/migrate.js down --migrations-dir ./src/migrations -v --config-file \"../../babel.config.json\"",
"refresh-database": "node ./scripts/refreshDatabase.js",
"test": "yarn workspace @tupaia/database check-test-database-exists && DB_NAME=tupaia_test mocha",
"test:coverage": "cross-env NODE_ENV=test nyc mocha",
"test:debug": "mocha --inspect-brk",
"update-test-data": "bash -c 'source .env && pg_dump -s -U $DB_USER -O $DB_NAME > src/tests/testData/testDataDump.sql && pg_dump -t migrations -c -U $DB_USER -O $DB_NAME >> src/tests/testData/testDataDump.sql'",
"test": "yarn package:test:withdb --runInBand",
"test:coverage": "yarn test --coverage",
"update-test-data": "bash -c 'source .env && pg_dump -s -U $DB_USER -O $DB_NAME > src/__tests__/testData/testDataDump.sql && pg_dump -t migrations -c -U $DB_USER -O $DB_NAME >> src/__tests__/testData/testDataDump.sql'",
"setup-test-database": "DB_NAME=tupaia_test scripts/setupTestDatabase.sh",
"check-test-database-exists": "DB_NAME=tupaia_test scripts/checkTestDatabaseExists.sh"
},
Expand All @@ -53,8 +52,6 @@
"devDependencies": {
"@babel/node": "^7.10.5",
"cross-env": "^7.0.2",
"deep-equal-in-any-order": "^1.0.27",
"mocha": "^8.1.3",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"pluralize": "^8.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/database/scripts/setupTestDatabase.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fi
PGPASSWORD=$DB_PG_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_PG_USER -c "DROP DATABASE IF EXISTS $DB_NAME"
PGPASSWORD=$DB_PG_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_PG_USER -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER"
PGPASSWORD=$DB_PG_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_PG_USER -c "ALTER USER $DB_USER WITH SUPERUSER"
PGPASSWORD=$DB_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_USER -d $DB_NAME -f ./src/tests/testData/testDataDump.sql
PGPASSWORD=$DB_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_USER -d $DB_NAME -f ./src/__tests__/testData/testDataDump.sql
PGPASSWORD=$DB_PG_PASSWORD psql -h $DB_URL -p $DB_PORT -U $DB_PG_USER -c "ALTER USER $DB_USER WITH NOSUPERUSER"

echo "Installing mvrefresh"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { expect } from 'chai';
import {
getTestModels,
populateTestData,
Expand Down Expand Up @@ -58,7 +57,7 @@ describe('AnalyticsRefresher', () => {
const matchingAnalytics = remainingAnalytics.filter(analytic =>
matchingFields.every(field => analytic[field] === expectedAnalytic[field]),
);
expect(matchingAnalytics.length).to.equal(
expect(matchingAnalytics.length).toBe(
1,
`No matching analytic found.\nExpected:\n${JSON.stringify(
expectedAnalytic,
Expand All @@ -69,7 +68,7 @@ describe('AnalyticsRefresher', () => {
);
matchedAnalytics = matchedAnalytics.concat(matchingAnalytics);
});
expect(remainingAnalytics.length).to.equal(
expect(remainingAnalytics.length).toBe(
0,
`Unexpected analytics remaining: ${remainingAnalytics.map(JSON.stringify)}`,
); // All analytics were matched
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

import { expect } from 'chai';
import sinon from 'sinon';
import winston from 'winston';

import { sleep } from '@tupaia/utils';
Expand All @@ -28,56 +26,54 @@ describe('ChangeHandler', () => {
};

handleChanges() {
sinon.stub().resolves();
jest.fn().mockResolvedValue();
}

resetMocks = () => {
this.changeTranslators = {
project: sinon.stub().callsFake(changeDetails => [changeDetails.new_record?.id]),
user: sinon.stub().callsFake(changeDetails => [changeDetails.new_record?.id]),
project: jest.fn(changeDetails => [changeDetails.new_record?.id]),
user: jest.fn(changeDetails => [changeDetails.new_record?.id]),
};
this.handleChanges = sinon.stub().resolves();
this.handleChanges = jest.fn().mockResolvedValue();
};
}

const changeHandler = new TestChangeHandler(models);
changeHandler.setDebounceTime(DEBOUNCE_TIME);

before(async () => {
beforeAll(async () => {
changeHandler.listenForChanges();
});

beforeEach(() => {
changeHandler.resetMocks();
});

after(() => {
afterAll(() => {
changeHandler.stopListeningForChanges();
});

it('is not triggered if a record type with no translator is mutated', async () => {
await upsertDummyRecord(models.indicator);
await models.database.waitForAllChangeHandlers();

expect(changeHandler.handleChanges).to.have.callCount(0);
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(0);
});

it('is triggered if a record type with a translator is mutated', async () => {
await upsertDummyRecord(models.project);
await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.callCount(1);
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(1);

await upsertDummyRecord(models.user);
await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.callCount(2);
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(2);
});

it('translates changes before handling them', async () => {
const record = await upsertDummyRecord(models.project);
await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.been.calledOnceWith(sinon.match.object, [
record.id,
]);
expect(changeHandler.handleChanges).toHaveBeenCalledOnceWith(expect.any(Object), [record.id]);
});

it('handles multiple changes in batches', async () => {
Expand All @@ -101,7 +97,7 @@ describe('ChangeHandler', () => {
await sleep(sleepTime);

await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.callCount(1);
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(1);
});

it('uses FIFO order', async () => {
Expand All @@ -116,30 +112,24 @@ describe('ChangeHandler', () => {

const projectIds1 = await submitProjectBatch();
await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.been.calledOnceWith(
sinon.match.object,
projectIds1,
);
expect(changeHandler.handleChanges).toHaveBeenCalledOnceWith(expect.any(Object), projectIds1);
changeHandler.resetMocks();

const projectIds2 = await submitProjectBatch();
await models.database.waitForAllChangeHandlers();
expect(changeHandler.handleChanges).to.have.been.calledOnceWith(
sinon.match.object,
projectIds2,
);
expect(changeHandler.handleChanges).toHaveBeenCalledOnceWith(expect.any(Object), projectIds2);
});

it('only runs one queue handler at a time', async () => {
let resolveOnQueueHandlerStart;
let isQueueHandlerRunning = false;
let queueHandlingCount = 0;

changeHandler.handleChanges = sinon.stub().callsFake(async () => {
changeHandler.handleChanges = jest.fn(async () => {
if (resolveOnQueueHandlerStart) {
resolveOnQueueHandlerStart();
}
expect(isQueueHandlerRunning).to.equal(false); // assert against concurrent handlers
expect(isQueueHandlerRunning).toBe(false); // assert against concurrent handlers
isQueueHandlerRunning = true;
queueHandlingCount++;
// sleep for longer than the debounce time so that we're still handling when the next one
Expand All @@ -156,8 +146,8 @@ describe('ChangeHandler', () => {

// after queue handler one has started but not yet completed, schedule another queue handler
await handlerOneStarted;
expect(isQueueHandlerRunning).to.equal(true);
expect(queueHandlingCount).to.equal(1);
expect(isQueueHandlerRunning).toBe(true);
expect(queueHandlingCount).toBe(1);
changeHandler.scheduleChangeQueueHandler();
const handlerTwoStarted = new Promise(resolve => {
resolveOnQueueHandlerStart = resolve;
Expand All @@ -166,14 +156,14 @@ describe('ChangeHandler', () => {
// wait for longer than the debounce time, so the just scheduled queue handler would want
// to start, but should still be processing the first handler
await sleep(DEBOUNCE_TIME + 10);
expect(isQueueHandlerRunning).to.equal(true);
expect(queueHandlingCount).to.equal(1);
expect(isQueueHandlerRunning).toBe(true);
expect(queueHandlingCount).toBe(1);

// after handler two has started but not yet completed, schedule another few queue handlers
// these should debounce and result in one more handler
await handlerTwoStarted;
expect(isQueueHandlerRunning).to.equal(true);
expect(queueHandlingCount).to.equal(2);
expect(isQueueHandlerRunning).toBe(true);
expect(queueHandlingCount).toBe(2);
changeHandler.scheduleChangeQueueHandler();
changeHandler.scheduleChangeQueueHandler();
changeHandler.scheduleChangeQueueHandler();
Expand All @@ -182,63 +172,63 @@ describe('ChangeHandler', () => {
// wait for longer than the debounce time, so the just scheduled handler would want to start,
// but should still be running the second handler
await sleep(DEBOUNCE_TIME + 10);
expect(isQueueHandlerRunning).to.equal(true);
expect(queueHandlingCount).to.equal(2);
expect(isQueueHandlerRunning).toBe(true);
expect(queueHandlingCount).toBe(2);

// wait for final handler to complete, then check a total of three were called
await finalHandlerPromise;
expect(isQueueHandlerRunning).to.equal(false);
expect(queueHandlingCount).to.equal(3);
expect(changeHandler.handleChanges).to.have.been.calledThrice;
expect(isQueueHandlerRunning).toBe(false);
expect(queueHandlingCount).toBe(3);
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(3);
});

describe('failure handling', () => {
const rejectedId = generateTestId();
const acceptedId = generateTestId();
let processedIds = [];

before(() => {
sinon.stub(winston, 'error');
beforeAll(() => {
jest.spyOn(winston, 'error').mockClear().mockImplementation();
});

beforeEach(() => {
processedIds = [];
changeHandler.handleChanges = sinon.stub().callsFake((transactingModels, changeIds) => {
changeHandler.handleChanges = jest.fn((transactingModels, changeIds) => {
if (changeIds.includes(rejectedId)) {
throw new Error(`Rejected id found: ${rejectedId}`);
}
processedIds.push(...changeIds);
});
winston.error.resetHistory();
winston.error.mockReset();
});

after(() => {
winston.error.restore();
afterAll(() => {
winston.error.mockRestore();
});

it('retries to handle a batch up to 3 times, then stops trying and logs an error', async () => {
await upsertDummyRecord(models.project, { id: rejectedId });
await models.database.waitForAllChangeHandlers();

expect(changeHandler.handleChanges).to.have.callCount(3);
expect(changeHandler.changeQueue).to.have.length(0);
expect(winston.error).to.have.been.calledOnceWith(
sinon.match(new RegExp(`Failed ids: ${rejectedId}`)),
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(3);
expect(changeHandler.changeQueue).toHaveLength(0);
expect(winston.error).toHaveBeenCalledOnceWith(
expect.objectContaining(new RegExp(`Failed ids: ${rejectedId}`)),
);
expect(processedIds).to.deep.equal([]);
expect(processedIds).toStrictEqual([]);
});

it('the whole batch fails if an error occurs', async () => {
await upsertDummyRecord(models.project, { id: rejectedId });
await upsertDummyRecord(models.project, { id: acceptedId });
await models.database.waitForAllChangeHandlers();

expect(changeHandler.handleChanges).to.have.callCount(3); // 3 retry attempts
expect(changeHandler.changeQueue).to.have.length(0);
expect(winston.error).to.have.been.calledOnceWith(
sinon.match(new RegExp(`Failed ids: ${[rejectedId, acceptedId]}`)),
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(3); // 3 retry attempts
expect(changeHandler.changeQueue).toHaveLength(0);
expect(winston.error).toHaveBeenCalledOnceWith(
expect.objectContaining(new RegExp(`Failed ids: ${[rejectedId, acceptedId]}`)),
);
expect(processedIds).to.deep.equal([]);
expect(processedIds).toStrictEqual([]);
});

it('valid future changes can still be queued and handled successfully', async () => {
Expand All @@ -248,12 +238,12 @@ describe('ChangeHandler', () => {
await models.database.waitForAllChangeHandlers();

// 3 failed attempts for the rejected project + 1 successful for the accepted project
expect(changeHandler.handleChanges).to.have.callCount(4);
expect(changeHandler.changeQueue).to.have.length(0);
expect(winston.error).to.have.been.calledOnceWith(
sinon.match(new RegExp(`Failed ids: ${rejectedId}`)),
expect(changeHandler.handleChanges).toHaveBeenCalledTimes(4);
expect(changeHandler.changeQueue).toHaveLength(0);
expect(winston.error).toHaveBeenCalledOnceWith(
expect.objectContaining(new RegExp(`Failed ids: ${rejectedId}`)),
);
expect(processedIds).to.deep.equal([acceptedId]);
expect(processedIds).toStrictEqual([acceptedId]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { expect } from 'chai';
import {
getTestModels,
populateTestData,
Expand Down Expand Up @@ -59,7 +58,7 @@ describe('EntityHierarchyCacher', () => {
descendant_id: r.descendant_id,
generational_distance: r.generational_distance,
})),
).to.deep.equalInAnyOrder(relations);
).toIncludeSameMembers(relations);
};

beforeEach(async () => {
Expand All @@ -76,16 +75,19 @@ describe('EntityHierarchyCacher', () => {
await clearTestData(models.database);
});

describe('buildAndCacheProject', async () => {
describe('buildAndCacheProject', () => {
const assertProjectRelationsCorrectlyBuilt = async (projectCode, expected) => {
await assertRelationsMatch(projectCode, expected);
};

it('handles a hierarchy that is fully canonical', async () => {
await assertProjectRelationsCorrectlyBuilt('project_ocean_test', INITIAL_HIERARCHY_OCEAN);
});

it('handles a hierarchy that has entity relation links', async () => {
await assertProjectRelationsCorrectlyBuilt('project_storm_test', INITIAL_HIERARCHY_STORM);
});

it('handles a hierarchy that has a custom set of canonical types', async () => {
await buildAndCacheProject('project_wind_test');
await assertProjectRelationsCorrectlyBuilt('project_wind_test', INITIAL_HIERARCHY_WIND);
Expand Down Expand Up @@ -181,7 +183,7 @@ describe('EntityHierarchyCacher', () => {
);
});

describe('deletes a subtree and rebuilds when an entity relation parent_id changes', async () => {
describe('deletes a subtree and rebuilds when an entity relation parent_id changes', () => {
it('handles a change low down in the hierarchy', async () => {
// change the parent of the aba -> aaa entity to abb
await models.entityRelation.update(
Expand Down
Loading

0 comments on commit 9139e02

Please sign in to comment.