【问题标题】:Mock dependency in Jest with TypeScript使用 TypeScript 在 Jest 中模拟依赖
【发布时间】:2018-07-23 08:44:46
【问题描述】:

当测试在不同文件中具有依赖关系的模块并将该模块分配为 jest.mock 时,TypeScript 会给出一个错误,即方法 mockReturnThisOnce(或任何其他 jest.mock 方法)在依赖,这是因为它是以前键入的。

让 TypeScript 从 jest.mock 继承类型的正确方法是什么?

这是一个简单的例子。

依赖

const myDep = (name: string) => name;
export default myDep;

test.ts

import * as dep from '../depenendency';
jest.mock('../dependency');

it('should do what I need', () => {
  //this throws ts error
  // Property mockReturnValueOnce does not exist on type (name: string)....
  dep.default.mockReturnValueOnce('return')
}

我觉得这是一个非常常见的用例,不知道如何正确输入。

【问题讨论】:

  • 如果我没记错的话,你必须在导入之前模拟。只需切换前 2 行。但我不确定这一点。
  • @ThomasKleßen 通过 ES6 import 导入的模块首先被评估,无论您是否在导入之前添加了一些代码。所以这行不通。
  • @Thomas 对 jest.mock 的调用被提升到代码的顶部 - 我猜是开玩笑的魔法......(ref)但是,这会产生一些陷阱,例如当calling jest.mock() with the module factory parameter 因此将模拟函数命名为mock...

标签: javascript unit-testing typescript jestjs


【解决方案1】:

从 Jest 24.9.0 开始,您可以模拟并正确键入 Class/Object/function 和 Jest 属性。

jest.MockedFunction

jest.MockedClass

对于类型化的模拟,我们希望模拟对象类型包含模拟对象类型和 Jest 模拟类型的联合。

import foo from 'foo';
jest.mock('foo');

const mockedFoo = foo as jest.MockedFunction<typeof foo>;
// or: const mockedFooClass = foo as jest.MockedClass<typeof FooClass>;


mockedFoo.mockResolvedValue('mockResult');

// Or:
(mockedFoo.getSomething as jest.MockedFunction<typeof mockedFoo.getSomething>).mockResolvedValue('mockResult');

如您所见,您可以手动转换您需要的内容,或者您​​需要一些东西来遍历所有 foo 的属性/方法来键入/转换所有内容。

要做到这一点(深度模拟类型),您可以使用 Jest 中引入的 jest.mocked() 27.4.0

import foo from 'foo';
jest.mock('foo');

const mockedFoo = jest.mocked(foo, true); 

mockedFoo.mockImplementation() // correctly typed
mockedFoo.getSomething.mockImplementation() // also correctly typed

【讨论】:

    【解决方案2】:

    Artur Górski 评价最高的解决方案不适用于最后一个 TS 和 Jest。 使用MockedClass

    import SoundPlayer from '../sound-player';
    
    jest.mock('../sound-player'); // SoundPlayer is now a mock constructor
    
    const SoundPlayerMock = SoundPlayer as jest.MockedClass<typeof SoundPlayer>;
    

    【讨论】:

    • 注意:如果您要模拟的是一个函数,请改用MockedFunction
    【解决方案3】:

    使用来自ts-jestmocked 助手 如解释here

    // foo.spec.ts
    import { mocked } from 'ts-jest/utils'
    import { foo } from './foo'
    jest.mock('./foo')
    
    // here the whole foo var is mocked deeply
    const mockedFoo = mocked(foo, true)
    
    test('deep', () => {
      // there will be no TS error here, and you'll have completion in modern IDEs
      mockedFoo.a.b.c.hello('me')
      // same here
      expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1)
    })
    
    test('direct', () => {
      foo.name()
      // here only foo.name is mocked (or its methods if it's an object)
      expect(mocked(foo.name).mock.calls).toHaveLength(1)
    })
    

    【讨论】:

    【解决方案4】:

    针对 TypeScript 版本 3.x 和 4.x 测试了两种解决方案,两者都在转换所需的功能

    1) 使用 jest.MockedFunction

    import * as dep from './dependency';
    
    jest.mock('./dependency');
    
    const mockMyFunction = dep.myFunction as jest.MockedFunction<typeof dep.myFunction>;
    

    2) 使用 jest.Mock

    import * as dep from './dependency';
    
    jest.mock('./dependency');
    
    const mockMyFunction = dep.default as jest.Mock;
    

    这两种解决方案没有区别。第二个较短,因此我建议使用那个。

    两种转换解决方案都允许在 mockMyFunction 上调用任何笑话模拟函数,例如 mockReturnValuemockResolvedValue https://jestjs.io/docs/en/mock-function-api.html

    mockMyFunction.mockReturnValue('value');
    

    mockMyFunction可以正常用于expect

    expect(mockMyFunction).toHaveBeenCalledTimes(1);
    

    【讨论】:

    • 谢谢!我比公认的答案更喜欢这个,因为 a)它更容易阅读 IMO 和 b)它在 JSX 中工作而不会导致语法错误
    • 我得到“typeError: mockMyFunction.mockReturnValue is not a function”
    • @Spock 我已经使用 require 而不是 import 解决了
    【解决方案5】:

    这很丑陋,事实上摆脱这种丑陋是我什至看这个问题的原因,但是要从模块模拟中获得强类型,你可以这样做:

    const myDep = (require('./dependency') as import('./__mocks__/dependency')).default;
    
    jest.mock('./dependency');
    

    确保您需要'./dependency' 而不是直接模拟,否则您将获得两个不同的实例化。

    【讨论】:

      【解决方案6】:

      使用as jest.Mock,别无其他

      我能想到的在 ts-jest 中模拟导出为 default 的模块的最简洁方法实际上归结为将模块转换为 jest.Mock

      代码:

      import myDep from '../dependency' // No `* as` here
      
      jest.mock('../dependency')
      
      it('does what I need', () => {
        // Only diff with pure JavaScript is the presence of `as jest.Mock`
        (myDep as jest.Mock).mockReturnValueOnce('return')
      
        // Call function that calls the mocked module here
      
        // Notice there's no reference to `.default` below
        expect(myDep).toHaveBeenCalled()
      })
      

      好处:

      • 不需要在测试代码的任何地方引用 default 属性 - 您可以引用实际导出的函数名称,
      • 您可以使用相同的技术来模拟命名导出,
      • 在导入语句中没有* as
      • 没有使用 typeof 关键字的复杂转换,
      • 没有像 mocked 这样的额外依赖项。

      【讨论】:

        【解决方案7】:

        最近的一个库用 babel 插件解决了这个问题:https://github.com/userlike/joke

        例子:

        import { mock, mockSome } from 'userlike/joke';
        
        const dep = mock(import('./dependency'));
        
        // You can partially mock a module too, completely typesafe!
        // thisIsAMock has mock related methods
        // thisIsReal does not have mock related methods
        const { thisIsAMock, thisIsReal } = mockSome(import('./dependency2'), () => ({ 
          thisIsAMock: jest.fn() 
        }));
        
        it('should do what I need', () => {
          dep.mockReturnValueOnce('return');
        }
        

        请注意depmockReturnValueOnce 是完全类型安全的。最重要的是,tsserver 知道 depencency 已导入并分配给 dep,因此 tsserver 支持的所有自动重构也将起作用。

        注意:我维护图书馆。

        【讨论】:

          【解决方案8】:

          我在@types/jest找到了这个:

          /**
            * Wrap a function with mock definitions
            *
            * @example
            *
            *  import { myFunction } from "./library";
            *  jest.mock("./library");
            *
            *  const mockMyFunction = myFunction as jest.MockedFunction<typeof myFunction>;
            *  expect(mockMyFunction.mock.calls[0][0]).toBe(42);
          */
          

          注意:当您执行const mockMyFunction = myFunctionmockFunction.mockReturnValue('foo') 之类的操作时,您也是一个不断变化的myFunction

          来源:https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jest/index.d.ts#L1089

          【讨论】:

            【解决方案9】:

            as jest.Mock

            只需将函数转换为 jest.Mock 就可以了:

            (dep.default as jest.Mock).mockReturnValueOnce('return')
            
            

            【讨论】:

              【解决方案10】:

              这是我对 jest@24.8.0ts-jest@24.0.2 所做的:

              来源:

              class OAuth {
              
                static isLogIn() {
                  // return true/false;
                }
              
                static getOAuthService() {
                  // ...
                }
              }
              

              测试:

              import { OAuth } from '../src/to/the/OAuth'
              
              jest.mock('../src/utils/OAuth', () => ({
                OAuth: class {
                  public static getOAuthService() {
                    return {
                      getAuthorizationUrl() {
                        return '';
                      }
                    };
                  }
                }
              }));
              
              describe('createMeeting', () => {
                test('should call conferenceLoginBuild when not login', () => {
                  OAuth.isLogIn = jest.fn().mockImplementationOnce(() => {
                    return false;
                  });
              
                  // Other tests
                });
              });
              

              这是模拟非默认类及其静态方法的方法:

              jest.mock('../src/to/the/OAuth', () => ({
                OAuth: class {
                  public static getOAuthService() {
                    return {
                      getAuthorizationUrl() {
                        return '';
                      }
                    };
                  }
                }
              }));
              

              这里应该是从你的类的类型到jest.MockedClass 或类似的类型转换。但它总是以错误告终。所以我就直接用了,效果很好。

              test('Some test', () => {
                OAuth.isLogIn = jest.fn().mockImplementationOnce(() => {
                  return false;
                });
              });
              

              但是,如果它是一个函数,您可以模拟它并进行类型对话。

              jest.mock('../src/to/the/Conference', () => ({
                conferenceSuccessDataBuild: jest.fn(),
                conferenceLoginBuild: jest.fn()
              }));
              const mockedConferenceLoginBuild = conferenceLoginBuild as 
              jest.MockedFunction<
                typeof conferenceLoginBuild
              >;
              const mockedConferenceSuccessDataBuild = conferenceSuccessDataBuild as 
              jest.MockedFunction<
                typeof conferenceSuccessDataBuild
              >;
              

              【讨论】:

                【解决方案11】:

                我使用来自 @types/jest/index.d.ts 的模式,就在 Mocked 类型 def 的上方(第 515 行):

                import { Api } from "../api";
                jest.mock("../api");
                
                const myApi: jest.Mocked<Api> = new Api() as any;
                myApi.myApiMethod.mockImplementation(() => "test");
                

                【讨论】:

                • 我很确定你可以这样做const myApi = new Api() as jest.Mocked&lt;Api&gt;;
                • @neoflash:在 TypeScript 3.4 中不是严格模式 - 它会抱怨 Api 类型与jest.Mock&lt;Api&gt; 没有充分重叠。你必须使用const myApi = new Api() as any as jest.Mock&lt;Api&gt;,我会说上面的看起来比双重断言好一点。
                • @tuptus:严格模式对 3.4 来说是新鲜的吗?请问您有这方面的链接吗?
                • @elmpp:不确定你的意思。 “严格模式”是指在 tsconfig.json 中有 "strict": true。这涵盖了 noImplicitAnystrictNullChecks 等内容,因此您不必为它们单独设置为 true。
                • 如果API构造函数参数怎么办?打字稿抱怨我必须通过它们,即使我不需要。
                【解决方案12】:

                您可以使用类型转换,您的 test.ts 应该如下所示:

                import * as dep from '../dependency';
                jest.mock('../dependency');
                
                const mockedDependency = <jest.Mock<typeof dep.default>>dep.default;
                
                it('should do what I need', () => {
                  //this throws ts error
                  // Property mockReturnValueOnce does not exist on type (name: string)....
                  mockedDependency.mockReturnValueOnce('return');
                });
                

                TS 转译器不知道 jest.mock('../dependency'); 更改了 dep 的类型,因此您必须使用类型转换。由于导入的 dep 不是类型定义,因此您必须使用 typeof dep.default 获取其类型。

                这是我在使用 Jest 和 TS 的过程中发现的其他一些有用的模式

                当导入的元素是一个类时,你不必使用 typeof 例如:

                import { SomeClass } from './SomeClass';
                
                jest.mock('./SomeClass');
                
                const mockedClass = <jest.Mock<SomeClass>>SomeClass;
                

                当您必须模拟一些节点原生模块时,此解决方案也很有用:

                import { existsSync } from 'fs';
                
                jest.mock('fs');
                
                const mockedExistsSync = <jest.Mock<typeof existsSync>>existsSync;
                

                如果您不想使用 jest 自动模拟而更喜欢创建手动模拟

                import TestedClass from './TestedClass';
                import TestedClassDependency from './TestedClassDependency';
                
                const testedClassDependencyMock = jest.fn<TestedClassDependency>(() => ({
                  // implementation
                }));
                
                it('Should throw an error when calling playSomethingCool', () => {
                  const testedClass = new TestedClass(testedClassDependencyMock());
                });
                

                testedClassDependencyMock() 创建模拟对象实例 TestedClassDependency 可以是类或类型或接口

                【讨论】:

                • 我不得不使用 jest.fn(() =&gt;... 而不是 jest.fn&lt;TestedClassDependency&gt;(() =&gt;... (我刚刚删除了 jest.fn 之后的类型转换),因为 IntelliJ 正在抱怨。否则,这个答案对我有帮助,谢谢!在我的 package.json 中使用它:“@types/jest”:“^24.0.3”
                • 哼,它不再适用于最后一个 TS 版本和 jest 24 :(
                • @Reza 它是自动模拟的,jestjs.io/docs/en/es6-class-mocks#automatic-mock
                • &lt;jest.Mock&lt;SomeClass&gt;&gt;SomeClass 表达式对我产生了 TS 错误:Conversion of type 'typeof SomeClass' to type 'Mock&lt;SomeClass, any&gt;' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type 'typeof SomeClass' is missing the following properties from type 'Mock&lt;SomeClass, any&gt;': getMockName, mock, mockClear, mockReset, and 11 more.ts(2352)
                • @the21st 在这种情况下,您应该使用(SomeClass as unknown) as &lt;jest.Mock&lt;SomeClass&gt;&gt; 之类的东西。请注意,这段代码使用了另一种形式的类型转换,现在更受欢迎。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-10-17
                • 2020-09-03
                • 2018-09-07
                • 2018-04-09
                • 2016-10-27
                相关资源
                最近更新 更多