You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Jest测试导入的store2函数?React组件测试覆盖率问题

解决Jest测试中store2的clearAll()未被调用及覆盖率问题

问题描述

React应用中编写单元测试时,LogOutForDisagreement组件内的store.clearAll()代码行始终无法被测试覆盖,Jest断言该方法被调用失败,测试覆盖率仅83.33%,缺失第19行(store.clearAll())的覆盖。

组件代码(LogOutForDisagreement.js)

import React from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import ClipboardClip from './../ClipboardClip';

const store = require('store2');

function LogOutForDisagreement(props) {
    const handleClick = (e) => {
        e.preventDefault();
        props.onChange(
            props.props.user = {
                loggedIn: false,
                email: '',
                password: '',
                firstName: ''
            },
        );
        store.clearAll();
        //sessionStorage.clear();

    };
    return (
        <div id="logOutForDisagreementClipboard" className={props.className}>
            <Form className="form">
                <ClipboardClip />
                <div className="centeredPaper">
                    <p>Sorry, you must agree to our privacy policy to access the site. You must now log out and log back in to agree to the terms.</p>
                    <Button id="logOutForDisagreement" type="" onClick={handleClick}>
                        Log Out
                    </Button>
                </div>
            </Form>
        </div>
    );
}

export default LogOutForDisagreement;

原测试代码(LogOutForDisagreement.test.js)

import React from 'react';
import { render, fireEvent, cleanup } from '@testing-library/react';
import toJson from 'enzyme-to-json';
import LogOutForDisagreement from './LogOutForDisagreement';

const store = require('store2');

let props = {
  props:
  {
    user: {
      loggedIn: false,
      email: 'email@example.com',
      password:   '0cc3dd4ad221f83270f876908064fa67d43407aa4bd580731b30c58c4fe2d8de00464107e5a143594068461af21ece9fb542e2f254b6f895eea6a6627f4f7c438a42fb18af7f539c4be2456661a5beec4a561f9443988182445b3952a5d9321a2d725b24b151f79a2806432c848b0b0c02576676e3a5c6f0661b4e318ca4f3e134d066808fab8fdd5322ed5cf5ad68aae43254a8fefdb69809c1bfdc07fe0365f38baa424d0c059c3a9fbff1f3525dae410740b9719929ef3f34235da519591f0410a08438132600fa802079b8d6f372f6dc439eb1b100aed28bb55cc3c6dc8982644940bd506278943fa8e430836cb874283e9f4438aac04a817f86bd1606036f03d196a211bdd91ac683d4ec63fcd503aa97b53d5c2571d39855b2be5f77be80a7f767271c8789aec26c66530c22387007c704f96b1a76a47e8e13fb263a0a6b24b2959495d34b47e03bcb95e8af13b555c5c403ec01427182afa1bae35ff81224b051fde7a61bd9044ed74042444a05e06186eedacc38c0128ff7d70c',
      firstName: 'FirstName'
    },
    className: "logOutForDisagreement"
  }
};

describe('Test the log out for disagreement button', () => {

  it('renders correctly enzyme and has onclick', async () => {
    console.log("This is the store at the begining after initial load: {" + Object.entries(sessionStorage).map(([key, val] = sessionItem) => {
      return `${key}: ${val}`}) + "}");

    const { getByText } = render(<LogOutForDisagreement id="logOutForDisagreementClipboard" className={props.className} />);
    const spy = jest.spyOn(store, 'clearAll');

    await fireEvent.click(getByText("Log Out"));
    
    console.log("This is in the store after the event fires: {" + Object.entries(sessionStorage).map(([key, val] = sessionItem) => {
      return `${key}: ${val}`}) + "}");

    expect(spy).toHaveBeenCalled();

    expect(toJson(getByText)).toMatchSnapshot();
    //spy.mockRestore();
  });
});

测试报错信息

Test the log out for disagreement button › renders correctly enzyme and has onclick

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

      37 |       return `${key}: ${val}`}) + "}");
      38 |
    > 39 |     expect(spy).toHaveBeenCalled();
         |                 ^
      40 |
      41 |     expect(toJson(getByText)).toMatchSnapshot();
      42 |     //spy.mockRestore();
...
--------------------------------------------|---------|----------|---------|---------|----------------------------------
File                                        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------------------------|---------|----------|---------|---------|--------------------
...
 LogOutForDisagreement.js                  |   83.33 |      100 |     100 |   83.33 | 19

解决方案

1. 核心问题分析

  • 未传入onChange props:组件handleClick方法中调用了props.onChange,但测试时未传入该props,导致执行时抛出错误,中断后续代码(包括store.clearAll())的执行。
  • Spy监听对象不一致:测试中本地导入的store实例与组件内导入的实例可能并非同一对象,导致spy无法捕获调用。

2. 修改后的测试代码

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import toJson from 'enzyme-to-json';
import LogOutForDisagreement from './LogOutForDisagreement';

// 模拟整个store2模块,确保组件使用的是模拟对象
jest.mock('store2', () => ({
  clearAll: jest.fn()
}));
const store = require('store2');

describe('Test the log out for disagreement button', () => {
  it('renders correctly and triggers store.clearAll on click', () => {
    // 创建mock的onChange函数,避免执行报错
    const mockOnChange = jest.fn();
    const testProps = {
      className: "logOutForDisagreement",
      onChange: mockOnChange,
      props: {
        user: {
          loggedIn: true,
          email: 'email@example.com',
          firstName: 'FirstName'
        }
      }
    };

    const { getByText } = render(<LogOutForDisagreement {...testProps} />);
    
    fireEvent.click(getByText("Log Out"));

    // 验证onChange被正确调用
    expect(mockOnChange).toHaveBeenCalled();
    // 验证store.clearAll被调用
    expect(store.clearAll).toHaveBeenCalled();
    expect(toJson(getByText("Log Out"))).toMatchSnapshot();
  });
});

3. 可选:优化组件内的props处理

组件中直接修改props.props.user的写法违反React单向数据流原则,建议调整为:

props.onChange({
  ...props.props,
  user: {
    loggedIn: false,
    email: '',
    password: '',
    firstName: ''
  }
});

避免直接修改传入的props对象,提升组件健壮性。

内容的提问来源于stack exchange,提问作者Sandog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 07:05:14