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

9

7

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.

Excommunicative answered 15/4, 2022 at 22:53 Comment(9)
What happens if you change the export line to export {API as default};? Or respectively, if you put the export default right in front of the class definition?Irvin
may be this link could helpKapok
@Irvin - same error for both formatsExcommunicative
@Kapok Interesting. That IS the exact error I'm seeing, but I am not trying to mock this class at all. I wonder if there's some relationship between this error and anything I can glean from that links. Thanks, I'll at least check it out.Excommunicative
Please also check this link!Irvin
@Irvin Thanks, I'll take a look. My best guess right now is either something to do with webpack or babel that's causing issuesExcommunicative
If all these hints don’t help you should try to trim your issue to a minimal working example (code and setup) for us to reproduce.Irvin
Does the test works if you change the import from "utils/API" to "../path/to/utils/API", if so, it's probably missing some babel-plugin-module-resolver, or jest moduleMapper to map the aliasMyra
@RoniCastro Nope, I thought that might have been it and tried it already. I'm drawing a complete blank here at this point. I'll try to put up a minimally reproduceable example sometime soon, but I basically have to spin up a new Expo app to do thatExcommunicative
E
4

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.

Excommunicative answered 20/4, 2022 at 19:35 Comment(2)
Thank you! I was seeing the same problem and with this tip, I was able to find a stray export after my class that was causing it.Triform
I would upvote this, except it doesn't identify the cause. In my code this is definitely what's happening - module A imports B, which imports C, which imports D, which imports E, which instantiates a class X (which is not used in module A) which instantiates another class Y, but Y "is not a constructor" inside Jest tests. It would be difficult to avoid importing B (or otherwise avoid importing E) and there is no apparent reason why Y "is not a constructor" inside Jest.Headrest
T
20

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();
Tuberous answered 21/4, 2022 at 22:7 Comment(2)
I answered this already, I found the solution/issueExcommunicative
Ah cool, I missed that. Very interesting that it could cause that issue. As you mentioned, it's probably something funky going on with babel or webpack. Glad you solved it though!Tuberous
C
7

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.

Concinnous answered 20/4, 2022 at 14:40 Comment(3)
It was already set to true in my tsconfig.jsonExcommunicative
Plus all of the particular files involved are JS. I am converting my codebase over to TS, but only about halfway done, and none of these files have been done yetExcommunicative
So, your half project is js and other half is ts, then convert your code base completely to ts as there may be many conflicts. Then try delete the node_modules and reinstall it, then try testing the code.Concinnous
M
6

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.

Mammalogy answered 12/10, 2022 at 10:50 Comment(1)
Thanks a lot! The part get MyClass() is the solution when you have classes without the export defaultDyak
E
4

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.

Excommunicative answered 20/4, 2022 at 19:35 Comment(2)
Thank you! I was seeing the same problem and with this tip, I was able to find a stray export after my class that was causing it.Triform
I would upvote this, except it doesn't identify the cause. In my code this is definitely what's happening - module A imports B, which imports C, which imports D, which imports E, which instantiates a class X (which is not used in module A) which instantiates another class Y, but Y "is not a constructor" inside Jest tests. It would be difficult to avoid importing B (or otherwise avoid importing E) and there is no apparent reason why Y "is not a constructor" inside Jest.Headrest
R
2

You'll need to export it like this :

export default class API
Rafa answered 18/4, 2022 at 10:16 Comment(1)
Nope, doesn't change anything, same errorExcommunicative
S
2

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())
  },
}));
Shallop answered 8/11, 2022 at 11:15 Comment(0)
H
2

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.

Headrest answered 25/4, 2023 at 20:4 Comment(0)
D
1

You could try with:

utils/API.js

export default class API {
...
}

test.js

import API from "utils/API";

const api = new API();
Dinsmore answered 18/4, 2022 at 11:1 Comment(2)
Nope, no change. And it's so weird, because the code I have works perfectly in actual usage, but ONLY when I am trying to run "npm run test" does it fail.Excommunicative
Sorry @StevenMatthews , could you please provide us a minimal reproducible example? It could help a lot. Thank youDinsmore
S
0

Especially when mocking AWS SDK v2 on TypeScript and Jest, the approach from https://mcmap.net/q/1389281/-typeerror-_api-default-is-not-a-constructor-with-jest-tests 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
Smoothspoken answered 12/9 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.