TypeError: ... is not a constructor ERROR with JEST

35 Views Asked by At

I'm not sure if my CognitoService class is being exported as expected to my JEST configuration because I'm still having this issue. I tried changing the way that the class is being exported without any good results, also I reviewed my jest.config.js several times but everything seems to be ok, any ideas?

This is the issue on my test console (jest):

TypeError: cognito_utils_1.CognitoService is not a constructor

      23 |     );
      24 |
    > 25 |   const cognitoService: CognitoService = new CognitoService(
         |                                          ^
      26 |     COGNITO_URL,
      27 |     COGNITO_USERS_CLIENT_ID,

this is my unit test: index.test.ts

import { handler } from './index';
import { Context, APIGatewayProxyCallback } from 'aws-lambda';

import { CognitoService } from '@mylib/cognito-utils';

const createMockContext = (): Context => ({
  ...
});

const createMockCallback = (): APIGatewayProxyCallback => (error, result) => {};

describe('handler', () => {

  beforeEach(() => {
    jest.resetModules();
  
  });
  
  it('should return ...', async () => {
    const event = {
      httpMethod: 'PUT',
      request: {
        ...
      }
    };


    // HERE IS THE ERROR /////////////////////////////////////

    const mockGetUserBySub = jest.fn().mockResolvedValue(null);
    
    jest.mock('@mylib/cognito-utils', () => {
      return {
        CognitoService: jest.fn().mockImplementation(() => {
            return {
                getUserBySub: mockGetUserBySub
            }
        })
      }   
    });

    // HERE IS THE ERROR /////////////////////////////////////

    const context = createMockContext();
    const callback = createMockCallback();

    const result = await handler(event, context, callback);

    expect(result.statusCode).toBe(404);
    expect(result.body).toEqual(JSON.stringify({ message: 'User not found in cognito' }));
  });

});

this is the handler that I want to test: index.ts

import { Handler } from 'aws-lambda';

import { CognitoService } from '@mylib/cognito-utils';

export const handler: Handler = async (event) => {

  const cognitoService: CognitoService = new CognitoService(
    COGNITO_URL,
    COGNITO_USERS_CLIENT_ID,
    COGNITO_USERS_POOL_ID,
    COGNITO_REGION,
  );

  const { id: userId } = await cognitoService.getUserBySub('123123');

  if (!userId) {
    return {
      statusCode: 404,
      body: JSON.stringify({ message: 'User not found in cognito' }),
    };
  }

  return {
    statusCode: 404,
    body: JSON.stringify({ message: userId }),
  };
};

and this is my cognitoService class: cognito-service.ts

import {
    CognitoIdentityProviderClient,
    CognitoIdentityProviderClientConfig,
  } from '@aws-sdk/client-cognito-identity-provider';
  import { UserMetadataAttributes } from '@mylib/models-common';
  
  export class CognitoService {
    identityProviderClient: CognitoIdentityProviderClient;
  
    constructor(
      private COGNITO_URL: string,
      private COGNITO_CLIENT_ID: string,
      private COGNITO_POOL_ID: string,
      private COGNITO_REGION: string,
    ) {
      const config: CognitoIdentityProviderClientConfig = {
        region: COGNITO_REGION,
        endpoint: COGNITO_URL,
      };
      this.identityProviderClient = new CognitoIdentityProviderClient(config);
    }
  
    async getUserBySub(sub: string): Promise<UserMetadataAttributes> {
      if (sub) {
        ...
      }
      throw new Error('User sub required');
    }
  
  }
  
1

There are 1 best solutions below

0
Eric Haynes On

You can't use jest.mock inside of a test. It has to be a the top level. How jest's mocking actually works under the hood is:

  • read your whole file
  • extract all of the jest.mock statements
  • hack the mocked value in into the node require cache
  • rewrite the test file without those mock statements
  • execute the rewritten file

Note that this also adds an implicit constraint that you can't reference values outside of the mock function (with some caveats around const, but life will be easier if you just assume there are no exceptions :) )

Try something like:

import { CognitoService } from '@mylib/cognito-utils'

jest.mock('@mylib/cognito-utils', () => {
  return {
    // just mock the constructor
    CognitoService: jest.fn(),
  }
})

describe('handler', () => {
  let mockCognitoService: jest.Mocked<CognitoService>

  beforeEach(async () => {
    mockCognitoService = {
      getUserBySub: jest.fn(),
    } as any // <--- cast not needed unless your real class has more methods
    ;(CognitoService as jest.Mock).mockImplementation(() => mockCognitoService)
  })

  it('whatever', async () => {
    mockCognitoService.getUserBySub.mockResolvedValueOnce(null)
    // ...
  })
})

Also note that if there are other things in that lib that you don't want to mock, you can preserve them like:

jest.mock('@mylib/cognito-utils', () => {
  return {
    ...jest.requireActual('@mylib/cognito-utils')
    CognitoService: jest.fn(),
  }
})

Additional $0.02; your class doesn't actually allow null as a result. If you're mocking this case, then you likely don't have strict mode enabled. You should really consider doing that. It eliminates whole classes of errors around incorrect types, including null/undefined. Half of the language is disabled without it, resulting in code that's not valid JS, and not valid TS either.