Steven Matthews
Steven Matthews

Reputation: 11275

TypeError: _API.default is not a constructor with Jest tests

I have an API class that I am trying to use in a React app.

// API file

class API {
...
}

export default API;

// Other file
import API from "utils/API";

const api = new API();

And I am getting the error:

TypeError: _API.default is not a constructor

But.. it seems like my default is set?

My Jest setup is like this:

  "jest": {
    "setupFiles": [
      "./jestSetupFile.js"
    ],
    "testEnvironment": "jsdom",
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-router-native/.*|@invertase/react-native-apple-authentication/.*)"
    ]
  },

My strong guess is that this is due to a configuration of my babel, webpack or package.json.

What could be causing this?

Note, I want to be clear, this doesn't happen whatsoever in my main application, only in Jest testing


If I change it to a named export/import, I get this:

TypeError: _API.API is not a constructor

Extremely confusing behavior.

Upvotes: 7

Views: 30288

Answers (9)

panuv
panuv

Reputation: 31

Especially when mocking AWS SDK v2 on TypeScript and Jest, the approach from https://stackoverflow.com/a/74359821/9381126 worked, for example like this:

    jest.mock('aws-sdk/clients/sqs', () => ({
        __esModule: true,
        default: class SQS {
            constructor(_args) {}
            sendMessage = jest.fn(() => Promise.resolve());
        },
    }));

In my example case, the rest of the project won't work with the esModuleInterop in tsconfig - which is a global setting.

This way of mocking resolved the error of

TypeError: sqs_1.default is not a constructor

Upvotes: 0

Qwertie
Qwertie

Reputation: 17176

This can be caused by cyclic dependencies among modules. Here's an example based on real-life experience.

Suppose you define a data store for book information, with some caches to hold information and API classes for loading from the backend:

// Data.ts
import { Author, AuthorApi } from './Author';
import { Book, BookApi } from './Book';

class Data {
    books = new Cache<Book>(new BookApi());
    authors = new Cache<Author>(new AuthorApi());
}

// (In real life I'd install MobX for reactivity here)
class Cache<T> {
    constructor(public api: any) {}
    items: T[];
    //
    // ... other stuff ...
    //
}

export default new Data();

And we define Author and Book modules like this:

// Author.ts ----------------------------------------------------
export class Author {
    public constructor(public id: number, public name: string) {}
}

export class AuthorApi { } // doesn't matter what's in here

// Book.ts ------------------------------------------------------
import data from './Data';

export class Book {
    constructor(public authorId: number, public title: string, public publishYear: number) { }

    get author() { return data.authors.items.filter(a => a.id === this.authorId)[0]; }
}

export class BookApi { } // doesn't matter what's in here

Here there is a circular reference between Book and Data. In the main app we start off with import data from 'Data' and it works fine. But in our unit tests we start with import { Book } from 'Book' (or some other module that uses Book) and get this error:

 TypeError: _Book.BookApi is not a constructor

      3 |
      4 | class Data {
    > 5 |     books = new Cache<Book>(new BookApi());
        |                             ^
      6 |     authors = new Cache<Author>(new AuthorApi());
      7 | }
      8 |

This happens because when importing 'Data', 'Data' imports 'Book', but 'Book' is already being imported and has not finished being imported yet. Book.ts cannot be processed a second time, so Data.ts is processed before Book.ts is fully initialized, so BookApi doesn't exist yet when the error occurs.

A simple workaround is to import data from 'Data' before importing Book in the tests. In TypeScript, by default, this workaround only works if data is used in the tests somewhere (e.g. let _ = data;). A much better solution is to find a way to eliminate the cyclic dependency.

Upvotes: 2

Gavara.Suneel
Gavara.Suneel

Reputation: 616

I resolved this error by using below code.

jest.mock('YOUR_API_PATH', () => ({
 __esModule: true,
 default: // REPLICATE YOUR API CONSTRUCTOR BEHAVIOUR HERE BY ADDING CLASS
})

If you want to mock complete API class, please check the below snippet.

jest.mock('YOUR_API_PATH', () => ({
  __esModule: true,
  default: class {
    constructor(args) {
     this.var1 = args.var1
    }
    someMethod: jest.fn(() => Promise.resolve())
  },
}));

Upvotes: 2

Emile
Emile

Reputation: 11721

I'm adding this because the issue I had presented the same but has a slightly different setup.

I'm not exporting the class with default, i.e.

MyClass.ts

// with default
export default MyClass {
   public myMethod()
   {
      return 'result';
   }
}

// without default, which i'm doing in some instances.
export MyClass {
   public myMethod()
   {
      return 'result';
   }
}

When you don't have the default, the import syntax changes.

In a (jest) test if you follow the convention where you do have export default MyClass(){};

then the following works.

const MOCKED_METHOD_RESULT = 'test-result'
jest.mock("MyClass.ts", () => {
    // will work and let you check for constructor calls:
    return jest.fn().mockImplementation(function () {
        return {
            myMethod: () => {
                return MOCKED_METHOD_RESULT;
            },
   
        };
    });
});

However, if you don't have the default and or are trying to mock other classes etc. then you need to do the following.

Note, that the {get MyClass(){}} is the critical part, i believe you can swap out the jest.fn().mockImplementation() in favour of jest.fn(()=>{})

jest.mock("MyClass.ts", () => ({
    get MyClass() {
        return jest.fn().mockImplementation(function () {
            return {
               myMethod: () => {
                   return MOCKED_METHOD_RESULT;
               },
   
           };
        });
    },
}));

So the issue is the way in which you access the contents of the class your mocking. And the get part allows you to properly define class exports.

Upvotes: 7

Josh Aguilar
Josh Aguilar

Reputation: 2271

As mentioned by others, it would be helpful to see a minimum reproducible example.

However, there is one other possible cause. Are you mocking the API class in your test file at all? This problem can sometimes happen if a class is mistakenly mocked as an "object" as opposed to a function. An object cannot be instantiated with a "new" operator.

For example, say we have a class file utils/API like so:

class API {
  someMethod() {
    // Does stuff
  }
}

export default API;

The following is an "incorrect" way to mock this class and will throw a TypeError... is not a constructor error if the class is instantiated after the mock has been created.

import API from 'utils/API';

jest.mock('utils/API', () => {
  // Returns an object
  return {
    someMethod: () => {}
  };
})

// This will throw the error
const api = new API();

The following will mock the class as a function and will accept the new operator and will not throw the error.

import API from 'utils/API';

jest.mock('utils/API', () => {
  // Returns a function
  return jest.fn().mockImplementation(() => ({
    someMethod: () => {}
  }));
})

// This will not throw an error anymore
const api = new API();

Upvotes: 21

Steven Matthews
Steven Matthews

Reputation: 11275

This was ultimately due to additional code inside the file that I was exporting the class from.

import { store } from "root/App";

if (typeof store !== "undefined") {
  let storeState = store.getState();
  let profile = storeState.profile;
}

At the top, outside my class for some functionality I had been working on.

This caused the class default export to fail, but only in Jest, not in my actual application.

Upvotes: 4

hari hara sankar
hari hara sankar

Reputation: 318

Trying adding "esModuleInterop": true, in your tsconfig.json. BY default esModuleInterop is set to false or is not set. B setting esModuleInterop to true changes the behavior of the compiler and fixes some ES6 syntax errors. Refer the documentation here.

Upvotes: 8

Daniele Ricci
Daniele Ricci

Reputation: 15797

You could try with:

utils/API.js

export default class API {
...
}

test.js

import API from "utils/API";

const api = new API();

Upvotes: 1

Ankit Gupta
Ankit Gupta

Reputation: 64

You'll need to export it like this :

export default class API

Upvotes: 2

Related Questions