【问题标题】:Mocking method on default export class in Jest in TypescriptTypescript中Jest中默认导出类的模拟方法
【发布时间】:2022-01-23 02:43:30
【问题描述】:

上下文

我想测试一个自定义钩子,它依赖于@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,但是这会在所有测试中模拟它。我认为问题在于我想模拟的方法是类上的方法。

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 提供了多种模拟依赖项的方法,但是我无法使其工作。

jest.MockedClass

Firebase 默认导出一个类,但该类本身是封装的。

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

实际上它不会将其识别为一个类,因为它已被包装。


jest.MockedFunction

然后我决定使用函数来模拟它(而不是使用箭头函数)。通过这种方法,我能够走得更远,但是通过这种方法,我需要提供所有属性。我尝试了一段时间,但在添加 X 数量的属性后我放弃了(参见下面的代码 sn-p)。所以如果这是要走的路,我想知道如何自动模拟大部分内容。

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

jest.spyOn

最后一次尝试是使用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'.

我只能在对象上提出静态属性,它们是:

这就是我选择dynamicLinks.prototype 的原因,这在answer 中提出了建议。

【问题讨论】:

    标签: typescript react-native jestjs mocking react-native-firebase


    【解决方案1】:

    我更喜欢使用动态链接(或其他 firebase 功能)创建服务。很容易模拟。

    dynamicLinksService.ts

    import dynamicLinks from '@react-native-firebase/dynamic-links';
    
    export const getInitialLink = () => dynamicLinks().getInitialLink();
    

    useDynamicLink.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);
          }
        });
      }, []);
    };
    

    useDynamicLink.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');
      });
    });
    

    【讨论】:

      【解决方案2】:

      您的 jest.spyOn 需要一些工作。

      Jest.spyOn 与 mocks 不同,它在你所在的范围内清理它的 mock(并且它不是真正的 mock,直到你说明确地调用 mockImplentation 等。因此它是一个“间谍”。)你想不断地改变你的模拟,你应该使用 spyOn() 并在每个测试中模拟实现,以减少每次清除模拟的样板。两者都可以正常工作,但我会尝试 3。

      首先,删除动态链接的模拟,因为我们将改为监视每个特定测试并模拟那里的实现。

      其次,因为您直接调用导出的函数,所以您必须像这样导入和监视函数。

      import * as dynamicLinks from '@react-native-firebase/dynamic-links';
      
      const dynamicLinkSpy = jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplentation( ... )
      

      dynamicLinks 现在是 jest 监视的导出文件,它查找的函数是 dynamicLinks(),这是生产代码调用的函数。

      另一个错误来自添加 .prototype。你应该看看生产代码是如何调用它的,这就是测试应该如何模拟它。同样为此,您替换了 dynamicLinks 上的实现,您必须创建将从在该对象上调用的嵌套函数向下工作的返回值。此外,由于您使用的是 .then() ,因此您的生产代码期望在函数中解析 Promise。像这样;

      const dynamicLinkSpy = jest
        .spyOn(dynamicLinks, 'dynamicLinks')
        .mockImplementation(()=>{ return {getInitialLink: ()=> Promise.resolve('test')}} );
      

      现在,您可以像往常一样使用不同的返回值并期待不同的结果。另外,请记住您应该测试它是否被调用。如下:

      expect(dynamicLinkSpy).toHaveBeenCalled();
      

      【讨论】:

      • 非常感谢您的回答!不幸的是,我无法使这种方法发挥作用。我已经更新了我的问题以反映它为什么不起作用。基本上,因为它是对实例化类的方法调用,所以我们只能访问该导出的静态属性。分别是“SDK_VERSION”和“ShortLinkType”。在此处添加“getInitialLinks”会导致编译错误。
      猜你喜欢
      • 2021-04-14
      • 2021-12-28
      • 2018-03-20
      • 2019-02-15
      • 2021-12-08
      • 1970-01-01
      • 2021-03-03
      • 1970-01-01
      • 2019-04-16
      相关资源
      最近更新 更多