[英]Mocking method on default export class in Jest in Typescript
语境
我想测试一个自定义钩子,这取决于@react-native-firebase/dynamic-links
。 我们将@testing-library
用于 react-native 及其实用函数来测试钩子( @testing-library/react-hooks
)。
这是我想测试的钩子(这是一个简化的例子):
import { useEffect } from 'react';
import dynamicLinks from '@react-native-firebase/dynamic-links';
import { navigateFromBackground } from '../deeplink';
// Handles dynamic link when app is loaded from closed state.
export const useDynamicLink = (): void => {
useEffect(() => {
void dynamicLinks()
.getInitialLink()
.then((link) => {
if (link && link.url) {
navigateFromBackground(link.url);
}
});
}, []);
};
我希望getInitialLink
调用在每个单独的测试中返回一些东西。 我已经能够用jest.mock(...)
模拟getInitialLink
,但是这会模拟所有测试。 我认为问题在于我想模拟的方法是 class 上的方法。
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
jest.mock('../deeplink');
// IMPORTANT: You cannot mock constructors with arrow functions. New cannot be
// called on an arrow function.
jest.mock('@react-native-firebase/dynamic-links', () => {
return function () {
return {
getInitialLink: async () => ({
url: 'fake-link',
}),
};
};
});
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
// IMPORTANT: act wrapper is needed so that all events are handled before
// state is inspected by the test.
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
尝试
所以这可行,但我无法更改每个测试的返回值。 Jest 提供了多种模拟依赖项的方法,但是我无法使其工作。
Firebase 默认导出一个 class,但 class 本身是包装好的。
declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStatics<
FirebaseDynamicLinksTypes.Module,
FirebaseDynamicLinksTypes.Statics
>;
根据文档,您需要像下面描述的那样模拟它。
import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedClass<typeof dynamicLinks>;
但是,它会引发以下错误:
Type 'FirebaseModuleWithStatics<Module, Statics>' does not satisfy the constraint 'Constructable'.
Type 'FirebaseModuleWithStatics<Module, Statics>' provides no match for the signature 'new (...args: any[]): any'.
实际上,它不会将其识别为 class,因为它已被包装。
然后我决定使用 function 来模拟它(而不是使用箭头函数)。 通过这种方法,我能够走得更远,但是通过这种方法,我需要提供所有属性。 我尝试了一段时间,但在添加 X 数量的属性后我放弃了(见下面的代码片段)。 因此,如果这是通往 go 的方式,我想知道如何自动模拟大部分内容。
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedFunction<
typeof dynamicLinks
>;
jest.mock('../deeplink');
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
// eslint-disable-next-line prefer-arrow-callback
dynamicLinksMock.mockImplementationOnce(function () {
return {
buildLink: jest.fn(),
buildShortLink: jest.fn(),
app: {
options: {
appId: 'fake-app-id',
projectId: 'fake-project-id',
},
delete: jest.fn(),
utils: jest.fn(),
analytics: jest.fn(),
name: 'fake-name',
crashlytics: jest.fn(),
dynamicLinks: jest.fn(),
},
onLink: jest.fn(),
resolveLink: jest.fn(),
native: jest.fn(),
emitter: jest.fn(),
getInitialLink: async () => ({
minimumAppVersion: '123',
utmParameters: { 'fake-param': 'fake-value' },
url: 'fake-link',
}),
};
});
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
最后一次尝试是使用spyOn
,这在这种情况下似乎很合适。 因为它只会模拟特定的函数,但是当我尝试运行测试时这会引发运行时错误。
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import dynamicLinks from '@react-native-firebase/dynamic-links';
jest.mock('../deeplink');
// Ensure automock
jest.mock('@react-native-firebase/dynamic-links');
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
jest
.spyOn(dynamicLinks.prototype, 'getInitialLink')
.mockImplementationOnce(async () => 'test');
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
错误:
Cannot spy the getInitialLink property because it is not a function; undefined given instead
所以总而言之,我完全不知道如何模拟getInitialLink
方法。 如果有人可以提供任何建议或提示,将不胜感激!
编辑1:
根据@user275564 的建议,我尝试了以下方法:
jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplementation(() => {
return { getInitialLink: () => Promise.resolve('fake-link') };
});
不幸的是,由于以下错误,typescript 无法编译:
No overload matches this call.
Overload 1 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.
Overload 2 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.
我只能在 object 上提出 static 属性,它们是:
这就是为什么我选择了这个答案中建议的dynamicLinks.prototype
。
你的 jest.spyOn 需要一些工作。
Jest.spyOn 与模拟不同,因为它会在您所在的 scope 中清理其模拟(并且它不是真正的模拟,直到您明确调用 mockImplentation 等,因此它是一个“间谍”。)不断更改您的模拟,您应该在每个测试中使用 spyOn() 和 mocking 实现,以减少每次清除模拟的样板。 两者都可以正常工作,但我会尝试 3。
首先,删除动态链接的模拟,因为我们将监视每个特定的测试并模拟那里的实现。
其次,因为您要直接调用导出的 function,所以您必须像这样导入和监视 function。
import * as dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinkSpy = jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplentation( ... )
dynamicLinks 现在是导出的文件 jest spys 和它寻找的 function 是 dynamicLinks(),这是生产代码调用的。
另一个错误来自添加.prototype。 你应该看看生产代码是怎么调用它的,那测试应该是mocking吧。 同样为此,您替换了 dynamicLinks 上的实现,您必须创建将从 object 上调用的嵌套函数向下工作的返回值。 此外,由于您使用的是.then(),因此您的生产代码需要在 function 中解析 Promise。 像这样;
const dynamicLinkSpy = jest
.spyOn(dynamicLinks, 'dynamicLinks')
.mockImplementation(()=>{ return {getInitialLink: ()=> Promise.resolve('test')}} );
现在,您可以像往常一样使用不同的返回值并期待不同的结果。 另外,请记住您应该测试它是否被调用。 如下所示:
expect(dynamicLinkSpy).toHaveBeenCalled();
我更喜欢使用动态链接(或其他 firebase 函数)创建服务。 很容易嘲笑。
动态链接服务.ts
import dynamicLinks from '@react-native-firebase/dynamic-links';
export const getInitialLink = () => dynamicLinks().getInitialLink();
使用DynamicLink.ts
import { useEffect } from 'react';
import { navigateFromBackground } from '../deeplink';
import { getInitialLink } from './dynamicLinkService';
export const useDynamicLink = (): void => {
useEffect(() => {
getInitialLink().then((link) => {
if (link && link.url) {
navigateFromBackground(link.url);
}
});
}, []);
};
使用DynamicLink.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import { getInitialLink } from './dynamicLinkService';
import { useDynamicLink } from './useDynamicLink';
jest.mock('../deeplink', () => ({
navigateFromBackground: jest.fn(),
}));
jest.mock('./dynamicLinkService', () => ({
getInitialLink: jest.fn(),
}));
describe('The useDynamicLink', () => {
it('should not navigate when link in empty', async () => {
const getInitialLinkMock = getInitialLink as jest.Mock;
getInitialLinkMock.mockResolvedValue(null);
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).not.toHaveBeenCalled();
});
it('should navigate when link is exist', async () => {
const getInitialLinkMock = getInitialLink as jest.Mock;
getInitialLinkMock.mockResolvedValue({ url: 'www.google.com' });
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('www.google.com');
});
});
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.