使用 Jest 使用私有构造函数模拟 TypeScript 类

Bru*_*res 6 javascript unit-testing typescript jestjs

所以,我是 TypeScript 和 Jest 世界的新手。为简单起见,我省略了部分代码示例。

基本上,我有一个User具有私有构造函数的实体,因为我在这个类中使用了静态工厂方法。此工厂方法User在成功时返回一个实例,或者UserCreationFailure在某些提供的字段无效时返回一个s列表。

我的User实体看起来像这样(请注意,它只是一个简化的伪代码):

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

另外,我正在编写一个测试以确保我的工厂方法正常工作。我的测试是这样的:

it('should create user when all provided fields are valid', () => {
    // arrange
    // mock User class calling the private constructor
    const mockUser = MockUserClass('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // act
    const result = User.create('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});
Run Code Online (Sandbox Code Playgroud)

但似乎我不能使用 Jest 来模拟User具有私有构造函数的类。我希望在我的测试的安排阶段这个模拟的用户实例通过工厂方法将它与返回的用户实例进行比较。

所以,我的问题是:有没有办法使用 Jest 来模拟带有私有构造函数的 TypeScript 类,并将参数传递给它?

我查看了有关模拟的 Jest 文档,但没有找到有关此特定场景的任何信息。

谢谢您的帮助。

Ber*_*rte 6

有一个甚至不需要的构造函数方法的解决方法jest

您所要做的就是访问 的User prototype并将类型设置为any,这样您就可以自由访问它的私有内容,然后访问 constructor 并调用它。由于您正在调用构造函数,因此不要忘记使用关键字new

it('should create user when all provided fields are valid', () => {
    // arrange
    const mockUser = new (User.prototype as any).constructor('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // act
    const result = User.create('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});
Run Code Online (Sandbox Code Playgroud)

如果您想检查是否mockUser确实是一个User实例,只需使用泛型expect(result).toStrictEqual<User>(mockUser);,您就会发现它的工作原理就像一个魅力。


DDo*_*men 6

Bernardo Duarte的答案已经是一个解决方案,但我想添加一些额外的信息并提出另一种方法。

其他解决方案

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', 'bruno@geosapiens.com.br', '12345678');
    // act
    const result = User.create('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});
Run Code Online (Sandbox Code Playgroud)

您还可以使用辅助函数来调用私有构造函数:

function privateFactory<T=any>(cls: any, ...args: any[]): T {
    return new cls(...args);
}
Run Code Online (Sandbox Code Playgroud)

然后你可以像这样简单地使用它:

it('should create user when all provided fields are valid', () => {
    // arrange
    const mockUser = privateFactory<User>(User, 'Bruno', 'bruno@geosapiens.com.br', '12345678');
    // act
    const result = User.create('Bruno', 'bruno@geosapiens.com.br', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});
Run Code Online (Sandbox Code Playgroud)

解释

通常,类从Function原型继承可调用性,因此您的User类实现以下接口:

{ new(name: string, email: string, password: string): User }
Run Code Online (Sandbox Code Playgroud)

这意味着您可以调用new User('n', 'e', 'p')因为User支持new继承运算符。

当您将构造函数设置为私有时,您实际上从类中排除了原型可调用性:因为 TS 编译器User没有new运算符的定义。

由于 TS 被编译为 JS,因此方法的可访问性纯粹是理论上的,不会以任何方式编译。因此,您可以强制编译器将您的User类解释为另一种类型,最常见的是any因为更灵活:

/* 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');
Run Code Online (Sandbox Code Playgroud)

进一步的方法包括使用来ConstructorParameters<T>确保构造函数参数的类型检查:

new (User as new(...args: ConstructorParameters<User>) => User)('n', 'e', 'p');
Run Code Online (Sandbox Code Playgroud)

问题又是,为了使用ConstructorParameters你需要有一个公共构造函数。最后,您可以复制私有构造函数的签名以确保类型检查:

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');
Run Code Online (Sandbox Code Playgroud)

最后,您可以将类型断言封装到函数参数中的任何类型:

function privateFactory<T=any>(cls: any, ...args: any[]): T {
    return new cls(...args);
}
Run Code Online (Sandbox Code Playgroud)

在这里,当您传递任何内容作为cls参数时,它将自动推断为any变量。您可以将其称为构造函数 ( new cls(...args))。缺点是您可以在该函数中传递任何内容(privateFactory(undefined)仍然可以编译),但它可能会在运行时破坏所有内容。