From b60ee9add328187a1a466f04449dacd83e597deb Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 6 Aug 2024 15:42:35 +0300 Subject: [PATCH] test: moving unmatching integration tests to unit tests --- .../configurations/integration/jest.config.js | 8 +- tests/configurations/unit/jest.config.js | 13 +- tests/helpers/containerConfig.ts | 43 ++++++ tests/integration/helpers/containerConfig.ts | 6 +- tests/integration/jobProcessor.spec.ts | 144 ------------------ tests/unit/jobProcessor/JobProcessor.spec.ts | 100 ++++++------ tests/unit/jobProcessor/jobProcessorSetup.ts | 17 ++- tests/unit/mocks/testCasesData.ts | 49 ++++++ 8 files changed, 173 insertions(+), 207 deletions(-) create mode 100644 tests/helpers/containerConfig.ts delete mode 100644 tests/integration/jobProcessor.spec.ts create mode 100644 tests/unit/mocks/testCasesData.ts diff --git a/tests/configurations/integration/jest.config.js b/tests/configurations/integration/jest.config.js index 5ebd8bf..b741eb6 100644 --- a/tests/configurations/integration/jest.config.js +++ b/tests/configurations/integration/jest.config.js @@ -21,10 +21,10 @@ module.exports = { testEnvironment: 'node', coverageThreshold: { global: { - branches: 50, - functions: 50, - lines: 50, - statements: 0, + branches: 80, + functions: 80, + lines: 80, + statements: -10, }, }, }; diff --git a/tests/configurations/unit/jest.config.js b/tests/configurations/unit/jest.config.js index 4a9f32d..5d03244 100644 --- a/tests/configurations/unit/jest.config.js +++ b/tests/configurations/unit/jest.config.js @@ -14,6 +14,11 @@ module.exports = { '!**/routes/**', '!/src/*', ], + modulePathIgnorePatterns: [ + '/src/models/newJobHandler.ts', + '/src/models/swapJobHandler.ts', + '/src/models/updateJobHandler.ts', + ], coverageDirectory: '/coverage', reporters: [ 'default', @@ -25,10 +30,10 @@ module.exports = { testEnvironment: 'node', coverageThreshold: { global: { - branches: 50, - functions: 50, - lines: 50, - statements: 0, + branches: 80, + functions: 80, + lines: 80, + statements: -10, }, }, }; diff --git a/tests/helpers/containerConfig.ts b/tests/helpers/containerConfig.ts new file mode 100644 index 0000000..83f3150 --- /dev/null +++ b/tests/helpers/containerConfig.ts @@ -0,0 +1,43 @@ +import jsLogger from '@map-colonies/js-logger'; +import { container, instancePerContainerCachingFactory } from 'tsyringe'; +import { trace } from '@opentelemetry/api'; +import { InjectionObject } from '../../src/common/dependencyRegistration'; +import { configMock, getMock, hasMock, registerDefaultConfig } from '../unit/mocks/configMock'; +import { SERVICES } from '../../src/common/constants'; +import { queueClientFactory } from '../../src/containerConfig'; +import { IngestionJobsConfig } from '../../src/common/interfaces'; +import { validateAndGetHandlersTokens } from '../../src/utils/configUtil'; +import { NewJobHandler } from '../../src/models/newJobHandler'; +import { UpdateJobHandler } from '../../src/models/updateJobHandler'; +import { SwapJobHandler } from '../../src/models/swapJobHandler'; +import { JOB_HANDLER_FACTORY_SYMBOL, jobHandlerFactory } from '../../src/models/jobHandlerFactory'; + +function getTestContainerConfig(): InjectionObject[] { + registerDefaultConfig(); + + const ingestionConfig = configMock.get('jobManagement.ingestion.jobs'); + + const handlersTokens = validateAndGetHandlersTokens(ingestionConfig); + + return [ + { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, + { token: SERVICES.CONFIG, provider: { useValue: configMock } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + { token: SERVICES.QUEUE_CLIENT, provider: { useFactory: instancePerContainerCachingFactory(queueClientFactory) } }, + { token: JOB_HANDLER_FACTORY_SYMBOL, provider: { useFactory: instancePerContainerCachingFactory(jobHandlerFactory) } }, + { token: handlersTokens.Ingestion_New, provider: { useClass: NewJobHandler } }, + { token: handlersTokens.Ingestion_Update, provider: { useClass: UpdateJobHandler } }, + { token: handlersTokens.Ingestion_Swap_Update, provider: { useClass: SwapJobHandler } }, + ]; +} + +const resetContainer = (clearInstances = true): void => { + if (clearInstances) { + container.clearInstances(); + } + + getMock.mockReset(); + hasMock.mockReset(); +}; + +export { getTestContainerConfig, resetContainer }; diff --git a/tests/integration/helpers/containerConfig.ts b/tests/integration/helpers/containerConfig.ts index 1caf5a4..3c512a6 100644 --- a/tests/integration/helpers/containerConfig.ts +++ b/tests/integration/helpers/containerConfig.ts @@ -3,14 +3,14 @@ import { container, instancePerContainerCachingFactory } from 'tsyringe'; import { trace } from '@opentelemetry/api'; import { InjectionObject } from '../../../src/common/dependencyRegistration'; import { configMock, getMock, hasMock, registerDefaultConfig } from '../../unit/mocks/configMock'; -import { SERVICES } from '../../../src/common/constants'; -import { queueClientFactory } from '../../../src/containerConfig'; import { IngestionJobsConfig } from '../../../src/common/interfaces'; import { validateAndGetHandlersTokens } from '../../../src/utils/configUtil'; +import { SERVICES } from '../../../src/common/constants'; +import { JOB_HANDLER_FACTORY_SYMBOL, jobHandlerFactory } from '../../../src/models/jobHandlerFactory'; +import { queueClientFactory } from '../../../src/containerConfig'; import { NewJobHandler } from '../../../src/models/newJobHandler'; import { UpdateJobHandler } from '../../../src/models/updateJobHandler'; import { SwapJobHandler } from '../../../src/models/swapJobHandler'; -import { JOB_HANDLER_FACTORY_SYMBOL, jobHandlerFactory } from '../../../src/models/jobHandlerFactory'; function getTestContainerConfig(): InjectionObject[] { registerDefaultConfig(); diff --git a/tests/integration/jobProcessor.spec.ts b/tests/integration/jobProcessor.spec.ts deleted file mode 100644 index 9307eb0..0000000 --- a/tests/integration/jobProcessor.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import nock from 'nock'; -import { IConfig } from 'config'; -import { DependencyContainer } from 'tsyringe'; -import { TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; -import { JobProcessor } from '../../src/models/jobProcessor'; -import { SERVICES } from '../../src/common/constants'; -import { ingestionNewJob, ingestionSwapUpdateJob, ingestionUpdateJob } from '../unit/mocks/jobsMockData'; -import { - finalizeTaskForIngestionNew, - finalizeTaskForIngestionSwapUpdate, - finalizeTaskForIngestionUpdate, - initTaskForIngestionNew, - initTaskForIngestionSwapUpdate, - initTaskForIngestionUpdate, -} from '../unit/mocks/tasksMockData'; -import { registerDependencies } from '../../src/common/dependencyRegistration'; -import { getTestContainerConfig } from './helpers/containerConfig'; - -describe('JobProcessor', () => { - let jobProcessor: JobProcessor; - let jobManagerBaseUrl: string; - let config: IConfig; - let container: DependencyContainer; - let queueClient: QueueClient; - let heartbeatBaseUrl: string; - - beforeEach(() => { - container = registerDependencies(getTestContainerConfig()); - jobProcessor = container.resolve(JobProcessor); - config = container.resolve(SERVICES.CONFIG); - queueClient = container.resolve(SERVICES.QUEUE_CLIENT); - heartbeatBaseUrl = config.get('jobManagement.config.heartbeat.baseUrl'); - jobManagerBaseUrl = config.get('jobManagement.config.jobManagerBaseUrl'); - }); - - afterEach(() => { - jest.clearAllMocks(); - nock.cleanAll(); - }); - - describe('happy path', () => { - const ingestionTestCases = [ - { - jobType: ingestionNewJob.type, - taskType: initTaskForIngestionNew.type, - job: ingestionNewJob, - task: initTaskForIngestionNew, - }, - { - jobType: ingestionUpdateJob.type, - taskType: initTaskForIngestionNew.type, - job: ingestionUpdateJob, - task: initTaskForIngestionUpdate, - }, - { - jobType: ingestionSwapUpdateJob.type, - taskType: initTaskForIngestionSwapUpdate.type, - job: ingestionSwapUpdateJob, - task: initTaskForIngestionSwapUpdate, - }, - { - jobType: ingestionNewJob.type, - taskType: finalizeTaskForIngestionNew.type, - job: ingestionNewJob, - task: finalizeTaskForIngestionNew, - }, - { - jobType: ingestionUpdateJob.type, - taskType: finalizeTaskForIngestionUpdate.type, - job: ingestionUpdateJob, - task: finalizeTaskForIngestionUpdate, - }, - { - jobType: ingestionUpdateJob.type, - taskType: finalizeTaskForIngestionSwapUpdate.type, - job: ingestionUpdateJob, - task: finalizeTaskForIngestionSwapUpdate, - }, - ]; - - test.each(ingestionTestCases)( - 'dequeue $taskType task and get $jobType job with corresponding taskType', - async ({ jobType, taskType, job, task }) => { - const consumeTaskUrl = `/tasks/${jobType}/${taskType}/startPending`; - - nock.emitter.on('no match', () => { - nock(jobManagerBaseUrl).post(consumeTaskUrl).reply(404, undefined); - }); - - nock(jobManagerBaseUrl) - .post(consumeTaskUrl) - .reply(200, { ...task }) - .persist() - .get(`/jobs/${task.jobId}?shouldReturnTasks=false`) - .reply(200, { ...job }) - .persist(); - - nock(heartbeatBaseUrl).post(`/heartbeat/${task.id}`).reply(200, 'ok').persist(); - - const dequeueSpy = jest.spyOn(queueClient, 'dequeue'); - const getJobSpy = jest.spyOn(queueClient.jobManagerClient, 'getJob'); - - const jobAndTaskType = await jobProcessor['getJobWithTaskType'](); - - expect(dequeueSpy).toHaveBeenCalledWith(jobType, taskType); - expect(getJobSpy).toHaveBeenCalledWith(task.jobId); - expect(jobAndTaskType?.taskType).toEqual(taskType); - expect(jobAndTaskType?.job).toEqual(job); - - await queueClient.heartbeatClient.stop(task.id); - } - ); - }); - - describe('bad path', () => { - it('should handle job manager error', async () => { - const jobType = ingestionNewJob.type; - const taskType = initTaskForIngestionNew.type; - const consumeTaskUrl = `/tasks/${jobType}/${taskType}/startPending`; - - nock(jobManagerBaseUrl).post(consumeTaskUrl).reply(500); - - await expect(jobProcessor['getJobWithTaskType']()).rejects.toThrow(); - }); - }); - - it('should handle missing job', async () => { - const jobType = ingestionNewJob.type; - const taskType = initTaskForIngestionNew.type; - const consumeTaskUrl = `/tasks/${jobType}/${taskType}/startPending`; - - nock(jobManagerBaseUrl) - .post(consumeTaskUrl) - .reply(200, { ...initTaskForIngestionNew }) - .get(`/jobs/${initTaskForIngestionNew.jobId}?shouldReturnTasks=false`) - .reply(404, { message: 'Job not found' }); - - nock(heartbeatBaseUrl).post(`/heartbeat/${initTaskForIngestionNew.id}`).reply(200, 'ok').persist(); - - await expect(jobProcessor['getJobWithTaskType']()).rejects.toThrow('Job not found'); - - await queueClient.heartbeatClient.stop(initTaskForIngestionNew.id); - }); -}); diff --git a/tests/unit/jobProcessor/JobProcessor.spec.ts b/tests/unit/jobProcessor/JobProcessor.spec.ts index a330436..54db396 100644 --- a/tests/unit/jobProcessor/JobProcessor.spec.ts +++ b/tests/unit/jobProcessor/JobProcessor.spec.ts @@ -1,14 +1,8 @@ +import nock from 'nock'; import { IJobResponse, ITaskResponse } from '@map-colonies/mc-priority-queue'; import { registerDefaultConfig } from '../mocks/configMock'; -import { ingestionNewJob, ingestionUpdateJob } from '../mocks/jobsMockData'; -import { - finalizeTaskForIngestionNew, - finalizeTaskForIngestionSwapUpdate, - finalizeTaskForIngestionUpdate, - initTaskForIngestionNew, - initTaskForIngestionUpdate, -} from '../mocks/tasksMockData'; import { IJobHandler, IngestionConfig } from '../../../src/common/interfaces'; +import { finalizeTestCases, initTestCases } from '../mocks/testCasesData'; import { JobProcessorTestContext, setupJobProcessorTest } from './jobProcessorSetup'; jest.mock('timers/promises', () => ({ @@ -29,15 +23,17 @@ describe('JobProcessor', () => { beforeEach(() => { jest.clearAllMocks(); registerDefaultConfig(); - testContext = setupJobProcessorTest(); }); afterEach(() => { jest.clearAllTimers(); + nock.cleanAll(); }); describe('start', () => { it('should start polling and stop when stop is called', async () => { + testContext = setupJobProcessorTest({ useMockQueueClient: true }); + const { jobProcessor, mockDequeue, configMock } = testContext; const ingestionConfig = configMock.get('jobManagement.ingestion'); const dequeueIntervalMs = configMock.get('jobManagement.config.dequeueIntervalMs'); @@ -56,48 +52,8 @@ describe('JobProcessor', () => { }); describe('consumeAndProcess', () => { - const initTestCases = [ - { - jobType: ingestionNewJob.type, - taskType: initTaskForIngestionNew.type, - job: ingestionNewJob, - task: initTaskForIngestionNew, - }, - { - jobType: ingestionUpdateJob.type, - taskType: initTaskForIngestionNew.type, - job: ingestionUpdateJob, - task: initTaskForIngestionUpdate, - }, - { - jobType: ingestionUpdateJob.type, - taskType: initTaskForIngestionNew.type, - job: ingestionUpdateJob, - task: initTaskForIngestionUpdate, - }, - ]; - const finalizeTestCases = [ - { - jobType: ingestionNewJob.type, - taskType: finalizeTaskForIngestionNew.type, - job: ingestionNewJob, - task: finalizeTaskForIngestionNew, - }, - { - jobType: ingestionUpdateJob.type, - taskType: finalizeTaskForIngestionUpdate.type, - job: ingestionUpdateJob, - task: finalizeTaskForIngestionUpdate, - }, - { - jobType: ingestionUpdateJob.type, - taskType: finalizeTaskForIngestionSwapUpdate.type, - job: ingestionUpdateJob, - task: finalizeTaskForIngestionSwapUpdate, - }, - ]; - test.each(initTestCases)('should process job of type $jobType and init task successfully', async ({ job, task }) => { + testContext = setupJobProcessorTest({ useMockQueueClient: true }); const { jobProcessor, mockDequeue, mockGetJob, mockJobHandlerFactory, configMock } = testContext; const dequeueIntervalMs = configMock.get('jobManagement.config.dequeueIntervalMs'); @@ -121,6 +77,7 @@ describe('JobProcessor', () => { }); test.each(finalizeTestCases)('should process job of type $jobType and finalize task successfully', async ({ job, task }) => { + testContext = setupJobProcessorTest({ useMockQueueClient: true }); const { jobProcessor, mockDequeue, mockGetJob, mockJobHandlerFactory, configMock } = testContext; const dequeueIntervalMs = configMock.get('jobManagement.config.dequeueIntervalMs'); @@ -143,4 +100,47 @@ describe('JobProcessor', () => { expect(mockHandler.handleJobFinalize).toHaveBeenCalledWith(job); }); }); + + describe('getJobWithTaskType', () => { + test.each([...initTestCases, ...finalizeTestCases])( + 'dequeue $taskType task and get $jobType job with corresponding taskType', + async ({ jobType, taskType, job, task }) => { + jest.useRealTimers(); + + testContext = setupJobProcessorTest({ useMockQueueClient: false }); + + const { jobProcessor, configMock, queueClient } = testContext; + const jobManagerBaseUrl = configMock.get('jobManagement.config.jobManagerBaseUrl'); + const heartbeatBaseUrl = configMock.get('jobManagement.config.heartbeat.baseUrl'); + const consumeTaskUrl = `/tasks/${jobType}/${taskType}/startPending`; + const misMatchRegex = /^\/tasks\/[^/]+\/[^/]+\/startPending$/; + + nock.emitter.on('no match', () => { + nock(jobManagerBaseUrl).post(misMatchRegex).reply(404, undefined).persist(); + }); + + nock(jobManagerBaseUrl) + .post(consumeTaskUrl) + .reply(200, { ...task }) + .persist() + .get(`/jobs/${task.jobId}?shouldReturnTasks=false`) + .reply(200, { ...job }) + .persist(); + + nock(heartbeatBaseUrl).post(`/heartbeat/${task.id}`).reply(200, 'ok').persist(); + + const dequeueSpy = jest.spyOn(queueClient, 'dequeue'); + const getJobSpy = jest.spyOn(queueClient.jobManagerClient, 'getJob'); + + const jobAndTaskType = await jobProcessor['getJobWithTaskType'](); + + expect(dequeueSpy).toHaveBeenCalledWith(jobType, taskType); + expect(getJobSpy).toHaveBeenCalledWith(task.jobId); + expect(jobAndTaskType?.taskType).toEqual(taskType); + expect(jobAndTaskType?.job).toEqual(job); + + await queueClient.heartbeatClient.stop(task.id); + } + ); + }); }); diff --git a/tests/unit/jobProcessor/jobProcessorSetup.ts b/tests/unit/jobProcessor/jobProcessorSetup.ts index ce176ae..6909134 100644 --- a/tests/unit/jobProcessor/jobProcessorSetup.ts +++ b/tests/unit/jobProcessor/jobProcessorSetup.ts @@ -4,6 +4,7 @@ import { TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; import { JobProcessor } from '../../../src/models/jobProcessor'; import { JobHandlerFactory } from '../../../src/models/jobHandlerFactory'; import { configMock } from '../mocks/configMock'; +import { IJobManagerConfig } from '../../../src/common/interfaces'; export type MockDequeue = jest.MockedFunction<(jobType: string, taskType: string) => Promise | null>>; export type MockGetJob = jest.MockedFunction<(jobId: string) => Promise>>; @@ -14,9 +15,10 @@ export interface JobProcessorTestContext { mockDequeue: MockDequeue; mockGetJob: MockGetJob; configMock: typeof configMock; + queueClient: QueueClient; } -export function setupJobProcessorTest(): JobProcessorTestContext { +export function setupJobProcessorTest({ useMockQueueClient = false }: { useMockQueueClient?: boolean }): JobProcessorTestContext { const mockLogger = jsLogger({ enabled: false }); const mockJobHandlerFactory = jest.fn(); @@ -31,13 +33,24 @@ export function setupJobProcessorTest(): JobProcessorTestContext { }, } as unknown as jest.Mocked; - const jobProcessor = new JobProcessor(mockLogger, configMock, mockJobHandlerFactory, mockQueueClient); + const jobManagerConfig = configMock.get('jobManagement.config'); + const queueClientInstance = new QueueClient( + mockLogger, + jobManagerConfig.jobManagerBaseUrl, + jobManagerConfig.heartbeat.baseUrl, + jobManagerConfig.dequeueIntervalMs, + jobManagerConfig.heartbeat.intervalMs + ); + + const queueClient = useMockQueueClient ? mockQueueClient : queueClientInstance; + const jobProcessor = new JobProcessor(mockLogger, configMock, mockJobHandlerFactory, queueClient); return { jobProcessor, mockJobHandlerFactory, mockDequeue, mockGetJob, configMock, + queueClient, }; } diff --git a/tests/unit/mocks/testCasesData.ts b/tests/unit/mocks/testCasesData.ts new file mode 100644 index 0000000..cb1400a --- /dev/null +++ b/tests/unit/mocks/testCasesData.ts @@ -0,0 +1,49 @@ +import { ingestionNewJob, ingestionUpdateJob } from '../mocks/jobsMockData'; +import { + finalizeTaskForIngestionNew, + finalizeTaskForIngestionSwapUpdate, + finalizeTaskForIngestionUpdate, + initTaskForIngestionNew, + initTaskForIngestionUpdate, +} from '../mocks/tasksMockData'; + +export const initTestCases = [ + { + jobType: ingestionNewJob.type, + taskType: initTaskForIngestionNew.type, + job: ingestionNewJob, + task: initTaskForIngestionNew, + }, + { + jobType: ingestionUpdateJob.type, + taskType: initTaskForIngestionNew.type, + job: ingestionUpdateJob, + task: initTaskForIngestionUpdate, + }, + { + jobType: ingestionUpdateJob.type, + taskType: initTaskForIngestionNew.type, + job: ingestionUpdateJob, + task: initTaskForIngestionUpdate, + }, +]; +export const finalizeTestCases = [ + { + jobType: ingestionNewJob.type, + taskType: finalizeTaskForIngestionNew.type, + job: ingestionNewJob, + task: finalizeTaskForIngestionNew, + }, + { + jobType: ingestionUpdateJob.type, + taskType: finalizeTaskForIngestionUpdate.type, + job: ingestionUpdateJob, + task: finalizeTaskForIngestionUpdate, + }, + { + jobType: ingestionUpdateJob.type, + taskType: finalizeTaskForIngestionSwapUpdate.type, + job: ingestionUpdateJob, + task: finalizeTaskForIngestionSwapUpdate, + }, +];