单元测试 - 使用 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);,你会发现它运行得非常顺利。
