【问题标题】:Mocking globals in Jest在 Jest 中模拟全局变量
【发布时间】:2017-03-19 20:48:41
【问题描述】:

在 Jest 中是否有任何方法可以模拟全局对象,例如 navigatorImage*?我几乎已经放弃了这一点,并把它留给了一系列可模拟的实用方法。例如:

// Utils.js
export isOnline() {
    return navigator.onLine;
}

测试这个微小的函数很简单,但很麻烦,而且根本没有确定性。我可以到达那里的 75%,但这大概是我能做到的:

// Utils.test.js
it('knows if it is online', () => {
    const { isOnline } = require('path/to/Utils');

    expect(() => isOnline()).not.toThrow();
    expect(typeof isOnline()).toBe('boolean');
});

另一方面,如果我对这种间接方式没问题,我现在可以通过这些实用程序访问navigator

// Foo.js
import { isOnline } from './Utils';

export default class Foo {
    doSomethingOnline() {
        if (!isOnline()) throw new Error('Not online');

        /* More implementation */            
    }
}

...并像这样进行确定性测试...

// Foo.test.js
it('throws when offline', () => {
    const Utils = require('../services/Utils');
    Utils.isOnline = jest.fn(() => isOnline);

    const Foo = require('../path/to/Foo').default;
    let foo = new Foo();

    // User is offline -- should fail
    let isOnline = false;
    expect(() => foo.doSomethingOnline()).toThrow();

    // User is online -- should be okay
    isOnline = true;
    expect(() => foo.doSomethingOnline()).not.toThrow();
});

在我使用过的所有测试框架中,Jest 感觉是最完整的解决方案,但每当我为了使其可测试而编写笨拙的代码时,我觉得我的测试工具让我失望了。

这是唯一的解决方案还是我需要添加 Rewire?

*别傻笑。 Image 非常适合 ping 远程网络资源。

【问题讨论】:

    标签: javascript unit-testing dependencies jestjs babel-jest


    【解决方案1】:

    由于每个测试套件都运行自己的环境,您可以通过覆盖它们来模拟全局变量。所有全局变量都可以通过global 命名空间访问:

    global.navigator = {
      onLine: true
    }
    

    覆盖仅对您当前的测试有效,不会影响其他测试。这也是处理Math.randomDate.now 的好方法。

    请注意,通过 jsdom 的一些更改,您可能必须像这样模拟全局变量:

    Object.defineProperty(globalObject, key, { value, writable: true });
    

    【讨论】:

    • global会和浏览器中的window一样吗?
    • 是的,你可以在那里设置东西。但也许不是window 中的所有内容也出现在global 中。这就是我不使用global.navigator.onLine 的原因,因为我不确定global 中是否有navigator 对象。
    • 请注意,作为一般做法,如今并非所有全局属性都是可覆盖的。有些具有可写的 false 并且会忽略值更改尝试。
    • "覆盖只对您当前的测试有效,不会影响其他测试。" - 这在任何地方都有记录吗?
    • @JamesPlayer 我可以肯定地确认,一个测试中的覆盖影响其他测试。至少在一个测试套件中。
    【解决方案2】:

    Jest 可能在编写接受的答案后发生了变化,但 Jest 在测试后似乎不会重置您的全局。请参阅随附的测试用例。

    https://repl.it/repls/DecentPlushDeals

    据我所知,解决此问题的唯一方法是使用afterEach()afterAll() 清理您对global 的分配。

    let originalGlobal = global;
    afterEach(() => {
      delete global.x;
    })
    
    describe('Scope 1', () => {
      it('should assign globals locally', () => {
        global.x = "tomato";
        expect(global.x).toBeTruthy()
      });  
    });
    
    describe('Scope 2', () => {
      it('should not remember globals in subsequent test cases', () => {
        expect(global.x).toBeFalsy();
      })
    });
    

    【讨论】:

    • 我遇到了同样的行为,每次测试运行后我的全局变量都没有重置。在afterEach() 中调用jest.clearAllMocks(); 帮助了我
    • 在 Angular ... import { global } from '@angular/compiler/src/util'
    • 由于测试可以并行运行,即使在afterEach() 中调用jest.clearAllMocks() 也可能会失败。
    【解决方案3】:

    如果有人需要使用 静态属性 模拟 global,那么我的示例应该会有所帮助:

      beforeAll(() => {
        global.EventSource = jest.fn(() => ({
          readyState: 0,
          close: jest.fn()
        }))
    
        global.EventSource.CONNECTING = 0
        global.EventSource.OPEN = 1
        global.EventSource.CLOSED = 2
      })
    

    【讨论】:

      【解决方案4】:

      如果您使用react-testing-library 并使用库提供的cleanup 方法,一旦文件中的所有测试运行,它将删除该文件中的所有全局声明。这将不会延续到任何其他运行的测试。

      例子:

      import { cleanup } from 'react-testing-library'
      
      afterEach(cleanup)
      
      global.getSelection = () => {
      
      }
      
      describe('test', () => {
        expect(true).toBeTruthy()
      })
      

      【讨论】:

      【解决方案5】:

      这样做的正确方法是使用spyOn。这里的其他答案,即使它们有效,也不要考虑清理和污染全局范围。

      // beforeAll
      jest
        .spyOn(window, 'navigator', 'get')
        .mockImplementation(() => { ... })
      
      // afterAll
      jest.restoreAllMocks();
      

      【讨论】:

      • 这给了我“Property navigator does not have access type get”——这应该是哪个版本的 Jest?
      • 试试:jest.spyOn('window.navigator', 'get')
      【解决方案6】:

      如果您需要分配重新分配window.navigator 上的属性值,那么您需要:

      1. 声明一个非常量变量
      2. 从全局/窗口对象返回
      3. 更改该原始变量的值(通过引用)

      这将防止在尝试重新分配 window.navigator 上的值时出错,因为这些大多是只读的。

      let mockUserAgent = "";
      
      beforeAll(() => {
        Object.defineProperty(global.navigator, "userAgent", {
          get() {
            return mockUserAgent;
          },
        });
      });
      
      it("returns the newly set attribute", () => {
        mockUserAgent = "secret-agent";
        expect(window.navigator.userAgent).toEqual("secret-agent");
      });
      

      【讨论】:

        猜你喜欢
        • 2018-12-15
        • 2016-11-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-22
        • 1970-01-01
        • 2020-10-09
        相关资源
        最近更新 更多