Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock TypeScript class with private constructor using Jest

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 UserCreationFailures 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.

like image 334
Bruno Peres Avatar asked Mar 04 '20 16:03

Bruno Peres


2 Answers

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.

like image 94
Bernardo Duarte Avatar answered Oct 24 '22 15:10

Bernardo Duarte


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.

Other Solution

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);
});

Explanation

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.

like image 28
DDomen Avatar answered Oct 24 '22 15:10

DDomen