Reputation: 11275
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
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
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
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
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
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
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
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
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