Commit f148847a authored by Savas Vedova's avatar Savas Vedova

Merge branch 'ps-simplify-test-setup' into 'master'

Move unit/integration test setup to shared module

See merge request gitlab-org/gitlab!74432
parents af4001a4 84fbc4d1
......@@ -716,16 +716,19 @@ Jest supports [manual module mocks](https://jestjs.io/docs/manual-mocks) by plac
If a manual mock is needed for a `node_modules` package, use the `spec/frontend/__mocks__` folder. Here's an example of
a [Jest mock for the package `monaco-editor`](https://gitlab.com/gitlab-org/gitlab/-/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1).
If a manual mock is needed for a CE module, place it in `spec/frontend/mocks/ce`.
If a manual mock is needed for a CE module, place the implementation in
`spec/frontend/__helpers__/mocks` and add a line to the `frontend/test_setup`
(or the `frontend/shared_test_setup`) that looks something like:
- Files in `spec/frontend/mocks/ce` mocks the corresponding CE module from `app/assets/javascripts`, mirroring the source module's path.
- Example: `spec/frontend/mocks/ce/lib/utils/axios_utils` mocks the module `~/lib/utils/axios_utils`.
- We don't support mocking EE modules yet.
- If a mock is found for which a source module doesn't exist, the test suite fails. 'Virtual' mocks, or mocks that don't have a 1-to-1 association with a source module, are not supported yet.
```javascript
// "~/lib/utils/axios_utils" is the path to the real module
// "helpers/mocks/axios_utils" is the path to the mocked implementation
jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));
```
#### Manual mock examples
- [`mocks/axios_utils`](https://gitlab.com/gitlab-org/gitlab/-/blob/bd20aeb64c4eed117831556c54b40ff4aee9bfd1/spec/frontend/mocks/ce/lib/utils/axios_utils.js#L1) -
- [`__helpers__/mocks/axios_utils`](https://gitlab.com/gitlab-org/gitlab/-/blob/a50edd12b3b1531389624086b6381a042c8143ef/spec/frontend/__helpers__/mocks/axios_utils.js#L1) -
This mock is helpful because we don't want any unmocked requests to pass any tests. Also, we are able to inject some test helpers such as `axios.waitForAll`.
- [`__mocks__/mousetrap/index.js`](https://gitlab.com/gitlab-org/gitlab/-/blob/cd4c086d894226445be9d18294a060ba46572435/spec/frontend/__mocks__/mousetrap/index.js#L1) -
This mock is helpful because the module itself uses AMD format which webpack understands, but is incompatible with the jest environment. This mock doesn't remove
......
......@@ -24,4 +24,5 @@ module.exports = {
'^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1',
},
}),
timers: 'real',
};
/* Common setup for both unit and integration test environments */
import { config as testUtilsConfig } from '@vue/test-utils';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { loadHTMLFixture, setHTMLFixture } from './fixtures';
import { TEST_HOST } from './test_constants';
import customMatchers from './matchers';
import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
// This module has some fairly decent visual test coverage in it's own repository.
jest.mock('@gitlab/favicon-overlay');
process.on('unhandledRejection', global.promiseRejectionHandler);
// Fake the `Date` for the rest of the jest spec runtime environment.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.use(Translate);
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
});
const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Exclude these jQuery matchers
if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
expect.extend({
[matcherName]: matcherFactory().compare,
});
});
expect.extend(customMatchers);
testUtilsConfig.deprecationWarningHandler = (method, message) => {
const ALLOWED_DEPRECATED_METHODS = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/295679
'finding components with `find` or `get`',
// https://gitlab.com/gitlab-org/gitlab/-/issues/295680
'finding components with `findAll`',
];
if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
global.console.error(message);
}
};
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
});
},
cancelIdleCallback(id) {
clearTimeout(id);
},
});
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
// each other, and removes the need to tidy up if it was changed for a given
// test.
setWindowLocation(TEST_HOST);
});
/**
* @module
*
* This module implements auto-injected manual mocks that are cleaner than Jest's approach.
*
* See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html
*/
import fs from 'fs';
import path from 'path';
import readdir from 'readdir-enhanced';
const MAX_DEPTH = 20;
const prefixMap = [
// E.g. the mock ce/foo/bar maps to require path ~/foo/bar
{ mocksRoot: 'ce', requirePrefix: '~' },
// { mocksRoot: 'ee', requirePrefix: 'ee' }, // We'll deal with EE-specific mocks later
// { mocksRoot: 'virtual', requirePrefix: '' }, // We'll deal with virtual mocks later
];
const mockFileFilter = (stats) => stats.isFile() && stats.path.endsWith('.js');
const getMockFiles = (root) => readdir.sync(root, { deep: MAX_DEPTH, filter: mockFileFilter });
// Function that performs setting a mock. This has to be overridden by the unit test, because
// jest.setMock can't be overwritten across files.
// Use require() because jest.setMock expects the CommonJS exports object
const defaultSetMock = (srcPath, mockPath) =>
jest.mock(srcPath, () => jest.requireActual(mockPath));
export const setupManualMocks = function setupManualMocks(setMock = defaultSetMock) {
prefixMap.forEach(({ mocksRoot, requirePrefix }) => {
const mocksRootAbsolute = path.join(__dirname, mocksRoot);
if (!fs.existsSync(mocksRootAbsolute)) {
return;
}
getMockFiles(path.join(__dirname, mocksRoot)).forEach((mockPath) => {
const mockPathNoExt = mockPath.substring(0, mockPath.length - path.extname(mockPath).length);
const sourcePath = path.join(requirePrefix, mockPathNoExt);
const mockPathRelative = `./${path.join(mocksRoot, mockPathNoExt)}`;
try {
setMock(sourcePath, mockPathRelative);
} catch (e) {
if (e.message.includes('Could not locate module')) {
// The corresponding mocked module doesn't exist. Raise a better error.
// Eventualy, we may support virtual mocks (mocks whose path doesn't directly correspond
// to a module, like with the `ee_else_ce` prefix).
throw new Error(
`A manual mock was defined for module ${sourcePath}, but the module doesn't exist!`,
);
}
}
});
});
};
/* eslint-disable global-require */
import path from 'path';
import axios from '~/lib/utils/axios_utils';
const absPath = path.join.bind(null, __dirname);
jest.mock('fs');
jest.mock('readdir-enhanced');
describe('mocks_helper.js', () => {
let setupManualMocks;
const setMock = jest.fn().mockName('setMock');
let fs;
let readdir;
beforeAll(() => {
jest.resetModules();
jest.setMock = jest.fn().mockName('jest.setMock');
fs = require('fs');
readdir = require('readdir-enhanced');
// We need to provide setupManualMocks with a mock function that pretends to do the setup of
// the mock. This is because we can't mock jest.setMock across files.
setupManualMocks = () => require('./mocks_helper').setupManualMocks(setMock);
});
afterEach(() => {
fs.existsSync.mockReset();
readdir.sync.mockReset();
setMock.mockReset();
});
it('enumerates through mock file roots', () => {
setupManualMocks();
expect(fs.existsSync).toHaveBeenCalledTimes(1);
expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce'));
expect(readdir.sync).toHaveBeenCalledTimes(0);
});
it("doesn't traverse the directory tree infinitely", () => {
fs.existsSync.mockReturnValue(true);
readdir.sync.mockReturnValue([]);
setupManualMocks();
const readdirSpy = readdir.sync;
expect(readdirSpy).toHaveBeenCalled();
readdirSpy.mock.calls.forEach((call) => {
expect(call[1].deep).toBeLessThan(100);
});
});
it('sets up mocks for CE (the ~/ prefix)', () => {
fs.existsSync.mockImplementation((root) => root.endsWith('ce'));
readdir.sync.mockReturnValue(['root.js', 'lib/utils/util.js']);
setupManualMocks();
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
expect(setMock).toHaveBeenCalledTimes(2);
expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
});
it('sets up mocks for all roots', () => {
const files = {
[absPath('ce')]: ['root', 'lib/utils/util'],
[absPath('node')]: ['jquery', '@babel/core'],
};
fs.existsSync.mockReturnValue(true);
readdir.sync.mockImplementation((root) => files[root]);
setupManualMocks();
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
expect(setMock).toHaveBeenCalledTimes(2);
expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
});
it('fails when given a virtual mock', () => {
fs.existsSync.mockImplementation((p) => p.endsWith('ce'));
readdir.sync.mockReturnValue(['virtual', 'shouldntBeImported']);
setMock.mockImplementation(() => {
throw new Error('Could not locate module');
});
expect(setupManualMocks).toThrow(
new Error("A manual mock was defined for module ~/virtual, but the module doesn't exist!"),
);
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
});
describe('auto-injection', () => {
it('handles ambiguous paths', () => {
jest.isolateModules(() => {
const axios2 = require('../../../app/assets/javascripts/lib/utils/axios_utils').default;
expect(axios2.isMock).toBe(true);
});
});
it('survives jest.isolateModules()', (done) => {
jest.isolateModules(() => {
const axios2 = require('~/lib/utils/axios_utils').default;
expect(axios2.isMock).toBe(true);
done();
});
});
it('can be unmocked and remocked', () => {
jest.dontMock('~/lib/utils/axios_utils');
jest.resetModules();
const axios2 = require('~/lib/utils/axios_utils').default;
expect(axios2).not.toBe(axios);
expect(axios2.isMock).toBeUndefined();
jest.doMock('~/lib/utils/axios_utils');
jest.resetModules();
const axios3 = require('~/lib/utils/axios_utils').default;
expect(axios3).not.toBe(axios2);
expect(axios3.isMock).toBe(true);
});
});
});
import { config as testUtilsConfig } from '@vue/test-utils';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Translate from '~/vue_shared/translate';
import { loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
import { initializeTestTimeout } from './__helpers__/timeout';
import customMatchers from './matchers';
import { setupManualMocks } from './mocks/mocks_helper';
/* Setup for unit test environment */
import 'helpers/shared_test_setup';
import { initializeTestTimeout } from 'helpers/timeout';
import './__helpers__/dom_shims';
import './__helpers__/jquery';
import '~/commons/bootstrap';
jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));
// This module has some fairly decent visual test coverage in it's own repository.
jest.mock('@gitlab/favicon-overlay');
process.on('unhandledRejection', global.promiseRejectionHandler);
setupManualMocks();
// Fake the `Date` for the rest of the jest spec runtime environment.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
initializeTestTimeout(process.env.CI ? 6000 : 500);
afterEach(() =>
// give Promises a bit more time so they fail the right test
......@@ -33,71 +13,3 @@ afterEach(() =>
jest.runOnlyPendingTimers();
}),
);
initializeTestTimeout(process.env.CI ? 6000 : 500);
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.use(Translate);
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
});
const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Exclude these jQuery matchers
if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
expect.extend({
[matcherName]: matcherFactory().compare,
});
});
expect.extend(customMatchers);
testUtilsConfig.deprecationWarningHandler = (method, message) => {
const ALLOWED_DEPRECATED_METHODS = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/295679
'finding components with `find` or `get`',
// https://gitlab.com/gitlab-org/gitlab/-/issues/295680
'finding components with `findAll`',
];
if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
global.console.error(message);
}
};
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
});
},
cancelIdleCallback(id) {
clearTimeout(id);
},
});
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
// each other, and removes the need to tidy up if it was changed for a given
// test.
setWindowLocation(TEST_HOST);
});
import '../../../frontend/test_setup';
import 'helpers/shared_test_setup';
import './setup_globals';
import './setup_axios';
import './setup_serializers';
......
import { setTestTimeout } from 'helpers/timeout';
import { initializeTestTimeout } from 'helpers/timeout';
initializeTestTimeout(process.env.CI ? 20000 : 7000);
beforeEach(() => {
window.gon = {
api_version: 'v4',
relative_url_root: '',
};
setTestTimeout(7000);
jest.useRealTimers();
});
afterEach(() => {
jest.useFakeTimers();
});
......@@ -3242,11 +3242,6 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
......@@ -6077,11 +6072,6 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
dependencies:
is-glob "^4.0.1"
glob-to-regexp@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
"glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
......@@ -10177,14 +10167,6 @@ readable-stream@~2.0.6:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
readdir-enhanced@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/readdir-enhanced/-/readdir-enhanced-2.2.4.tgz#773fb8a8de5f645fb13d9403746d490d4facb3e6"
integrity sha512-JQD83C9gAs5B5j2j40qLn/K83HhR8po3bUonebNeuJQUZbbn7q1HxL9kQuPBtxoXkaUpbtEmpFBw5kzyYnnJDA==
dependencies:
call-me-maybe "^1.0.1"
glob-to-regexp "^0.4.0"
readdirp@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment