React组件mock document.body.append时出现appendChild未定义错误求助
如何正确Mock document.body.append避免React测试报错?
问题场景
有一个React组件Test,在useEffect中通过document.body.append添加内联script标签:
// Test.tsx import { useEffect } from 'react'; const Test = () => { useEffect(() => { const inlineScript = document.createElement('script'); inlineScript.setAttribute('data-name', 'john'); inlineScript.innerHTML = 'console.log(123);'; document.body.append(inlineScript); }, []); return <div>Test</div>; }; export default Test;
编写测试用例时,尝试用Object.defineProperty重写document.body来mockappend方法,执行测试时出现TypeError: baseElement.appendChild is not a function报错:
// Test.spec.tsx(错误写法) import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import Test from './Test'; Object.defineProperty(document, 'body', { value: { append: jest.fn() }, writable: true, configurable: true }); describe('<Test /> component test', () => { it('should work', () => { render(<Test />); }); });
依赖版本:
"react": "^17.0.2", "react-dom": "^17.0.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.4.3",
错误原因
直接替换整个document.body对象时,只保留了自定义的append方法,丢失了body原本的原生DOM方法(比如appendChild)。而@testing-library/react的render方法需要调用这些原生方法挂载组件,因此触发报错。
解决方案
不要完全替换document.body,而是在原有对象基础上mockappend方法,保留其他原生属性和方法。推荐用jest.spyOn实现,既安全又便于测试后恢复。
修改后的测试代码
// Test.spec.tsx(正确写法) import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import Test from './Test'; describe('<Test /> component test', () => { let appendMock: jest.SpyInstance; // 每个测试前mock append方法 beforeEach(() => { appendMock = jest.spyOn(document.body, 'append').mockImplementation(() => {}); }); // 每个测试后恢复原方法,避免影响其他用例 afterEach(() => { appendMock.mockRestore(); }); it('should call document.body.append with the correct script element', () => { render(<Test />); // 验证append调用次数 expect(appendMock).toHaveBeenCalledTimes(1); // 验证传入的script参数符合预期 const [scriptElement] = appendMock.mock.calls[0]; expect(scriptElement).toBeInstanceOf(HTMLScriptElement); expect(scriptElement.getAttribute('data-name')).toBe('john'); expect(scriptElement.innerHTML).toBe('console.log(123);'); }); });
替代方案(不推荐)
如果必须使用Object.defineProperty,需要继承原有body的所有属性和方法:
// Test.spec.tsx(替代写法) import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import Test from './Test'; describe('<Test /> component test', () => { let originalBody: HTMLElement; beforeEach(() => { originalBody = document.body; Object.defineProperty(document, 'body', { value: { ...originalBody, append: jest.fn() }, writable: true, configurable: true }); }); afterEach(() => { Object.defineProperty(document, 'body', { value: originalBody, writable: true, configurable: true }); }); it('should work', () => { render(<Test />); expect(document.body.append).toHaveBeenCalled(); }); });
这种方法需要手动保存和恢复原body对象,不如jest.spyOn简洁,优先推荐第一种方案。
内容的提问来源于stack exchange,提问作者LY Hooi
相关产品推荐
相关产品推荐

