在Jest中模拟`document`

84

我正在尝试使用jest为我的Web组件项目编写测试。我已经使用了带有es2015预设的babel。当加载js文件时,我遇到了一个问题。我遵循了一个代码片段,其中document对象具有currentScript对象。但在测试上下文中,它是null。因此,我想模拟相同的情况。但是jest.fn()并没有真正帮助解决这个问题。我该如何处理这个问题?

jest失败的代码片段。

var currentScriptElement = document._currentScript || document.currentScript;
var importDoc = currentScriptElement.ownerDocument;

我已经编写了一个测试用例。component.test.js

import * as Component from './sample-component.js';

describe('component test', function() {
  it('check instance', function() {
    console.log(Component);
    expect(Component).toBeDefined();
  });
});

以下是 Jest 抛出的错误信息:

Test suite failed to run

    TypeError: Cannot read property 'ownerDocument' of null

      at src/components/sample-component/sample-component.js:4:39

更新: 根据Andreas Köberle的建议,我添加了一些全局变量并尝试模拟如下

__DEV__.document.currentScript = document._currentScript = {
  ownerDocument: ''
};
__DEV__.window = {
  document: __DEV__.document
}
__DEV__.document.registerElement = jest.fn();

import * as Component from './arc-sample-component.js';

describe('component test', function() {
  it('check instance', function() {
    console.log(Component);
    expect(Component).toBeDefined();
  });
});

但是没有运气

更新:我已经尝试了上面的代码,没有使用__dev__。同时将文档设置为全局变量。


1
你尝试过使用 global.document 吗? - Pavithra Kodmad
1
是的,我已经尝试过了,但没有成功。 - thecodejack
1
所以我基本上像这样使用jsdom `const jsdom = require('jsdom');const documentHTML = '<!doctype html><html><body><div id="root"></div></body></html>'; global.document = jsdom.jsdom(documentHTML); ` 在此之后,我可以将任何我想要的内容添加到文档中,并且可以在我的测试中使用。 - Pavithra Kodmad
实际上问题在于,jsdom非常简单,没有Web组件API。无论如何,我已经根据我的答案暂时解决了它。 - thecodejack
9个回答

56

与其他人所说的类似,但不要试图自己模拟DOM,只需使用JSDOM:

// __mocks__/client.js

import { JSDOM } from "jsdom"
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window

然后在您的jest配置中:

    "setupFiles": [
      "./__mocks__/client.js"
    ],

1
TypeError: _jsdom.JSDOM is not a constructor - temporary_user_name
1
他们一年前改变了他们的API吗? - Patrick Lee Scott
1
看起来 new JSDOM() 仍然是正确的 - 也许你有其他配置不正确,比如 babel?https://github.com/jsdom/jsdom - Patrick Lee Scott
他们没有在发布版本时附上变更日志,所以很难说。https://github.com/jsdom/jsdom/releases - Patrick Lee Scott
更新日志请查看 https://github.com/jsdom/jsdom/blob/master/Changelog.md - Alf Eaton
1
是的,您需要安装 babel-jest 和 @babel/plugin-transform-modules-commonjs。它仍然可以与 Jest 24(我使用 ts-jest)一起使用,并且非常适合模拟其他全局变量,如 window.navigator 或 navigator :-) - barbara.post

25

我已经通过在Jest中使用setUpFiles属性来解决了这个问题。这将在jsdom之后和每个测试之前执行,非常适合我。

在Jest配置中设置setupFiles,例如:

"setupFiles": ["<rootDir>/browserMock.js"]


// browserMock.js
Object.defineProperty(document, 'currentScript', {
  value: document.createElement('script'),
});

理想情况是加载webcomponents.js以填充jsdom。


19

我一直在为我参与的一个项目进行模拟文件而苦苦挣扎。 我在React组件中调用document.querySelector(),需要确保它能正常工作。 最终,这是对我有用的内容:

it('should test something', () => {
  const spyFunc = jest.fn();
  Object.defineProperty(global.document, 'querySelector', { value: spyFunc });
  <run some test>
  expect(spyFunc).toHaveBeenCalled()
});

7
如果您和我一样需要模拟文档为未定义(例如用于服务器端/客户端测试),则可以在测试套件中使用object.defineProperty,而无需使用setupFiles。
示例:
beforeAll(() => {
  Object.defineProperty(global, 'document', {});
})

我试图通过在对象中提供document.referrerdocument.URL来模拟它们...但没有成功。在测试运行器中,这些属性仍然显示为它们通常的值。 - theUtherSide
@theUtherSide 我注意到这个解决方案在某些版本的Node上有效,而在其他版本上无效。也许安装NVM并尝试不同的版本会有所帮助。 - tctc91
谢谢你的建议!我会尝试使用不同版本的Node并发布一些发现!我真的比使用Jest配置更喜欢这种策略。我在一个大团队中工作,保持我们的单元测试隔离和原子化当然更好! - theUtherSide
确认 -- 我尝试过这个应用程序,它使用的是Node v8.11.1和Jest 21.2.1。 - theUtherSide
1
TypeError: Cannot redefine property: document => at JSDOMEnvironment.teardown (node_modules/jest-environment-jsdom/build/index.js:192:14) - Dimitri Kopriwa

6

这是我项目中名为 super-project 的文件夹结构:


  • super-project
    • config
      • __mocks__
        • dom.js
    • src
      • user.js
    • tests
      • user.test.js
    • jest.config.js
    • package.json

你需要设置Jest在测试中使用一个模拟:

dom.js:

import { JSDOM } from "jsdom"
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window

user.js:

export function create() {
  return document.createElement('table');  
}

user.test.js:

import { create } from "../src/user";

test('create table', () => {
  expect(create().outerHTML).toBe('<table></table>');
});

jest.config.js:

module.exports = {
  setupFiles: ["./config/__mocks__/dom.js"],
};

参考资料:

创建手动模拟:
https://jestjs.io/docs/en/manual-mocks.html

操作DOM对象:
https://jestjs.io/docs/en/tutorial-jquery

Jest配置:
https://jestjs.io/docs/en/configuration


6
如果您需要为属性定义测试值,则可以采用稍微细致的方法。每个属性都需要单独定义,并且还需要将属性设置为可写
Object.defineProperty(window.document, 'URL', {
  writable: true,
  value: 'someurl'
});

参见:https://github.com/facebook/jest/issues/890

我使用 Jest 21.2.1 和 Node v8.11.1 时,这个方法对我有效。


3
我可以使用Node.js的全局作用域模块来解决此问题,通过设置一个文档的模拟对象,以我的情况为例,是通过getElementsByClassName函数。
// My simple mock file
export default {
    getElementsByClassName: () => {
        return [{
            className: 'welcome'
        }]
    }
};

// Your test file
import document from './name.component.mock.js';
global.document = {
    getElementsByClassName: document.getElementsByClassName
};

1
是的,那确实可以,但这只是一个非常简单的用例...在webcomponents.js中,我们有大量互相关联的API。模拟它们是一项非常困难的任务... - thecodejack
但在你的情况下,你需要模拟什么? - Renato B.

2
我找到了另一种解决方案。假设在您的组件内部,您想通过类名(document.getElementsByClassName)获取DOM中的元素的引用。您可以按照以下步骤操作:
let wrapper
beforeEach(() => {
    wrapper = mount(<YourComponent/>)
    jest.spyOn(document, 'getElementsByClassName').mockImplementation(() => 
        [wrapper.find('.some-class').getDOMNode()]
    )
})

通过这种方式,您手动将getElementsByClassName的返回值设置为.some-class的引用。可能需要通过调用wrapper.setProps({})重新渲染组件。

希望这对您有所帮助!


-2
希望这能有所帮助。
const wrapper = document.createElement('div');
const render = shallow(<MockComponent{...props} />);
document.getElementById = jest.fn((id) => {
      wrapper.innerHTML = render.find(`#${id}`).html();
      return wrapper;
    });

1
这个答案假设该组件是一个React组件,但我没有看到任何迹象表明它是。在我看来,它看起来像一个普通的JavaScript Web组件。 - Greg Malcolm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接