Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[no-issue]: Convert database tests to jest #4188

Merged
merged 3 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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