So, I'm a newbie in the TypeScript and Jest world. I've omitted part of my code samples for the sake of simplicity.
Basically, I have a User
entity that has a private constructor because I'm using a static factory method in this class. This factory method returns a User
instance when success or a list of UserCreationFailure
s when some provided field is invalid.
My User
entity looks like this (please, note that it is just a simplified pseudo-code):
export class User {
// fields
private constructor(name: string, email: string, password: string) {
this.name = name;
this.email = email;
this.password = password;
}
public static create(name: string, email: string, password: string) : UserCreationFailure[], User {
// validations
return failures.length ? failures : new User(name, email, password;
}
}
Also, I'm writing a test to ensures that my factory method works fine. My test looks like this:
it('should create user when all provided fields are valid', () => {
// arrange
// mock User class calling the private constructor
const mockUser = MockUserClass('Bruno', '[email protected]', '12345678');
// act
const result = User.create('Bruno', '[email protected]', '12345678');
// assert
expect(result).toStrictEqual(mockUser);
});
But seems that I cannot use Jest to mock the User
class who has a private constructor. I want this mocked user instance in the arrange phase of my test to compare it with the returned user instance by the factory method.
So, my question is: is there a way to use Jest to mock a TypeScript class with a private constructor, passing parameters to it?
I took a look at the Jest documentation about mocks, But I didn't find anything about this specific scenario.
Thanks for the help.
There is a workaround the constructor method that doesn't even need jest
.
All you have to do is to access the User
prototype
's set the type as any
, sou you're free to access it's private contents, then access constructor
and call it. Since you're calling a constructor, don't forget to use the new
keyword.
it('should create user when all provided fields are valid', () => {
// arrange
const mockUser = new (User.prototype as any).constructor('Bruno', '[email protected]', '12345678');
// act
const result = User.create('Bruno', '[email protected]', '12345678');
// assert
expect(result).toStrictEqual(mockUser);
});
If you want to check if mockUser
is indeed a User
instance just use generics expect(result).toStrictEqual<User>(mockUser);
and you'll find that it works like a charm.
The Bernardo Duarte's answer is already a solution, but I would like to add some additional information to it and propose also another way.
it('should create user when all provided fields are valid', () => {
// arrange
// NOTE: here you don't need to access the prototype & constructor
// but you can directly use the class
const mockUser = new (User as any)('Bruno', '[email protected]', '12345678');
// act
const result = User.create('Bruno', '[email protected]', '12345678');
// assert
expect(result).toStrictEqual(mockUser);
});
You can also use an helper function to call private constructors:
function privateFactory<T=any>(cls: any, ...args: any[]): T {
return new cls(...args);
}
Then you can simply use it like this:
it('should create user when all provided fields are valid', () => {
// arrange
const mockUser = privateFactory<User>(User, 'Bruno', '[email protected]', '12345678');
// act
const result = User.create('Bruno', '[email protected]', '12345678');
// assert
expect(result).toStrictEqual(mockUser);
});
Usually a class inherits callability from the Function
prototype, so your User
class implements the following interface:
{ new(name: string, email: string, password: string): User }
This means that you can call new User('n', 'e', 'p')
because User
supports the new
operator for inheritance.
When you make a constructor private you are actually excluding the prototype callability from the class: for the TS compiler User
does not have a definition for new
operator.
Since TS is compiled to JS, the accessibility of a method is purely theoretical, it will not be compiled in any way. So you can force the compiler to interpret your User
class as another type, the most common is any
because is the more flexible:
/* TS */
new (User as any)('n', 'e', 'p');
/* JS */
new User('n', 'e', 'p')
// note you don't need to employ prototype at all
// as stated by Bernardo, but under the hood
// the JS interpret will do that anyway
/* TS */
new (User.prototype as any).constructor('n', 'e', 'p');
/* JS */
new User.prototype.constructor('n', 'e', 'p');
A further approach would include the use of ConstructorParameters<T>
to ensure the type check for the constructor parameters:
new (User as new(...args: ConstructorParameters<User>) => User)('n', 'e', 'p');
The problem is again that in order to use ConstructorParameters
you need to have a public constructor. At last you can copy the signature of the private constructor in order to ensure the type checking:
new (User as any as new(name: string, email: string, password: string) => User)('n', 'e', 'p');
// To generalize/clean/reuse just define an helper type
type Constructor<T, Args extends any[]> = new(...args: Args) => T;
new (User as any as Constructor<User, [string, string, string]>)('n', 'e', 'p');
Lastly you can encapsulate the type assertion to any in a function argument:
function privateFactory<T=any>(cls: any, ...args: any[]): T {
return new cls(...args);
}
Here when you pass anything as cls
parameter, it will be automatically inferred as a any
variable. You can call it as a constructor (new cls(...args)
). The downside is that you can pass literally anything in that function (privateFactory(undefined)
still compile) but it could break everything at runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With