网站搜索

单元测试 - 使用 Jest 模拟具有私有构造函数的 TypeScript 类


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

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

我的 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;
    }
}

另外,我正在编写一个测试,以确保我的工厂方法运行正常。我的测试如下所示:

it('should create user when all provided fields are valid', () => {
    // arrange
    // mock User class calling the private constructor
    const mockUser = MockUserClass('Bruno', '[email ', '12345678');
    // act
    const result = User.create('Bruno', '[email ', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});

但似乎我无法使用 Jest 来模拟具有私有构造函数的 User 类。我想在测试的安排阶段使用这个模拟的用户实例,以便将其与工厂方法返回的用户实例进行比较。

所以,我的问题是:是否有一种方法可以使用 Jest 模拟一个带有私有构造函数的 TypeScript 类,并向其传递参数?

我查阅了关于 Jest 模拟的文档,但没有找到关于这个特定情景的任何内容。

谢谢你的帮助。

答案#1

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', '[email ', '12345678');
    // act
    const result = User.create('Bruno', '[email ', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});

你也可以使用一个辅助函数来调用私有构造函数:

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

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

it('should create user when all provided fields are valid', () => {
    // arrange
    const mockUser = privateFactory<User>(User, 'Bruno', '[email ', '12345678');
    // act
    const result = User.create('Bruno', '[email ', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});

解释

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

{ new(name: string, email: string, password: string): User }

这意味着你可以调用 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');

另一种方法是使用 ConstructorParameters<T> 来确保构造函数参数的类型检查:

new (User as new(...args: ConstructorParameters<User>) => User)('n', 'e', 'p');

问题再次在于,为了使用 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');

最后,你可以将类型断言为 any 封装到函数参数中:

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

在这里,当你将任何东西作为 cls 参数传递时,它会自动被推断为 any 类型的变量。你可以将它作为构造函数调用(new cls(...args))。缺点是你可以在该函数中传递任何东西(privateFactory(undefined) 仍然可以编译),但这可能会在运行时破坏一切。

答案#2

有一种绕过构造函数的方法,甚至不需要 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', '[email ', '12345678');
    // act
    const result = User.create('Bruno', '[email ', '12345678');
    // assert
    expect(result).toStrictEqual(mockUser);
});

如果你想检查 mockUser 是否确实是 User 实例,只需使用泛型 expect(result).toStrictEqual<User>(mockUser);,你会发现它运行得非常顺利。

相关文章