简体   繁体   English

Mocking 默认导出方法 class 在 Typescript

[英]Mocking method on default export class in Jest in Typescript

Context语境

I would like to test a custom hook, that depends on @react-native-firebase/dynamic-links .我想测试一个自定义钩子,这取决于@react-native-firebase/dynamic-links We are using @testing-library for react-native and its utility functions to test hooks ( @testing-library/react-hooks ).我们将@testing-library用于 react-native 及其实用函数来测试钩子( @testing-library/react-hooks )。

This is the hook I would like to test (this is a simplified example):这是我想测试的钩子(这是一个简化的例子):

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

I would like the getInitialLink call to return something in each separate test.我希望getInitialLink调用在每个单独的测试中返回一些东西。 I have been able to mock getInitialLink with jest.mock(...) , however this mocks it for all tests.我已经能够用jest.mock(...)模拟getInitialLink ,但是这会模拟所有测试。 I think the trouble is that the method I would like to mock, is a method on a class.我认为问题在于我想模拟的方法是 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');
  });
});

Attempts尝试

So this works, but I am not able to change the return value for each test.所以这可行,但我无法更改每个测试的返回值。 Jest offers a wide variety of ways to mock dependencies, however I was not able to make it work. Jest 提供了多种模拟依赖项的方法,但是我无法使其工作。

jest.MockedClassjest.MockedClass

Firebase exports by default a class, but the class itself is wrapped. Firebase 默认导出一个 class,但 class 本身是包装好的。

declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStatics<
  FirebaseDynamicLinksTypes.Module,
  FirebaseDynamicLinksTypes.Statics
>;

According to the documentation, you would need to mock it like described below.根据文档,您需要像下面描述的那样模拟它。

import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedClass<typeof dynamicLinks>;

It however throws the following error:但是,它会引发以下错误:

Type 'FirebaseModuleWithStatics<Module, Statics>' does not satisfy the constraint 'Constructable'.
  Type 'FirebaseModuleWithStatics<Module, Statics>' provides no match for the signature 'new (...args: any[]): any'.

Effectively it does not recognise it as a class since it is wrapped.实际上,它不会将其识别为 class,因为它已被包装。


jest.MockedFunction jest.MockedFunction

I decided to then mock it by using a function (and not using arrow functions).然后我决定使用 function 来模拟它(而不是使用箭头函数)。 With this approach I was able to get a lot further, however with this approach I need to provide all properties.通过这种方法,我能够走得更远,但是通过这种方法,我需要提供所有属性。 I attempted this for a while, but I gave up after adding X amount of properties (see code snippet below).我尝试了一段时间,但在添加 X 数量的属性后我放弃了(见下面的代码片段)。 So if this is the way to go, I would like to know how to automock most of this.因此,如果这是通往 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');
  });
});

jest.spyOn jest.spyOn

The last attempt was to use spyOn which seems fitting in this case.最后一次尝试是使用spyOn ,这在这种情况下似乎很合适。 Since it will mock only specific functions, however this throws a runtime error when I try to run the tests.因为它只会模拟特定的函数,但是当我尝试运行测试时这会引发运行时错误。

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

Error:错误:

Cannot spy the getInitialLink property because it is not a function; undefined given instead

So all in all I am at a complete loss on how to mock the getInitialLink method.所以总而言之,我完全不知道如何模拟getInitialLink方法。 If anyone could provide any advice or tips it would be greatly appreciated!如果有人可以提供任何建议或提示,将不胜感激!


Edit 1:编辑1:

Based on the advice of @user275564 I tried the following:根据@user275564 的建议,我尝试了以下方法:

jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplementation(() => {
   return { getInitialLink: () => Promise.resolve('fake-link') };
});

Unfortunately typescript does not compile because of the following error:不幸的是,由于以下错误,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'.

I am only able to put forth the static properties on the object there which are:我只能在 object 上提出 static 属性,它们是: 在此处输入图像描述

This is why I went for the dynamicLinks.prototype which was suggested in this answer .这就是为什么我选择了这个答案中建议的dynamicLinks.prototype

Your jest.spyOn needs some work.你的 jest.spyOn 需要一些工作。

Jest.spyOn is different than mocks in that it cleans up its mock in the scope you are inside (and that it's not really a mock until you say explicitly call mockImplentation ect. on it thus it's a 'spy.') Since you want to constantly change your mocks you should be using spyOn() and mocking the implementation in each test to reduce boilerplate from clearing the mocks each time. Jest.spyOn 与模拟不同,因为它会在您所在的 scope 中清理其模拟(并且它不是真正的模拟,直到您明确调用 mockImplentation 等,因此它是一个“间谍”。)不断更改您的模拟,您应该在每个测试中使用 spyOn() 和 mocking 实现,以减少每次清除模拟的样板。 Both can work just fine though but I would work on attempt 3.两者都可以正常工作,但我会尝试 3。

First, remove the mock of dynamic links since we are going to spy on each specific test instead and mock the implementation there.首先,删除动态链接的模拟,因为我们将监视每个特定的测试并模拟那里的实现。

Second, because you are calling on an exported function directly you have to import and spy on the function like this.其次,因为您要直接调用导出的 function,所以您必须像这样导入和监视 function。

import * as dynamicLinks from '@react-native-firebase/dynamic-links';

const dynamicLinkSpy = jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplentation( ... )

dynamicLinks is now the exported file jest spys on and the function it looks for is dynamicLinks(), which is what the production code is calling. dynamicLinks 现在是导出的文件 jest spys 和它寻找的 function 是 dynamicLinks(),这是生产代码调用的。

Another error is from adding.prototype.另一个错误来自添加.prototype。 You should look at how the production code is calling it, that's how the tests should be mocking it.你应该看看生产代码是怎么调用它的,那测试应该是mocking吧。 Also for this, you replace implementation on dynamicLinks, you have to create the return value that will work downward from the nested functions being called on that object.同样为此,您替换了 dynamicLinks 上的实现,您必须创建将从 object 上调用的嵌套函数向下工作的返回值。 Also, since you're using.then() your production code expects a Promise to be resolved in the function.此外,由于您使用的是.then(),因此您的生产代码需要在 function 中解析 Promise。 Like so;像这样;

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

Now, you can play with different return values and expect different results as usual.现在,您可以像往常一样使用不同的返回值并期待不同的结果。 Also, remember you should test whether or not it's being called on.另外,请记住您应该测试它是否被调用。 Like below:如下所示:

expect(dynamicLinkSpy).toHaveBeenCalled();

I prefer to create a service using dynamic links (or other firebase functions).我更喜欢使用动态链接(或其他 firebase 函数)创建服务。 It's easy to mock.很容易嘲笑。

dynamicLinksService.ts动态链接服务.ts

import dynamicLinks from '@react-native-firebase/dynamic-links';

export const getInitialLink = () => dynamicLinks().getInitialLink();

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

useDynamicLink.test.ts使用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM