目錄

React 單元測試 - Test Was Not Wrap in Act ?

Warning: An update to Demo inside a test was not wrapped in act(…).

最近在寫單元測試,遇到這個神奇的 warning,記錄一下。

情境: 假設有一個元件,點了會更新 state 並呼叫 props.onChange

class 版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    export default class Demo extends Component {
      state = { sum: 0 };

      render() {
        const { sum } = this.state;

        return (
          <button
            onClick={() => {
              this.setState({ sum: sum + 1 });
              this.props.onChange(sum + 1);
            }}
          >
            click me
          </button>
        );
      }
    }

function 版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    const Demo = ({ onChange }) => {
      const [state, setState] = useState(0);

      return (
        <button onClick={() => { 
            setState((s) => s + 1) 
            onChange(state + 1)
        }}>
          click me
        </button>
      );
    };

我們可以這樣寫單元測試,測試 onChange 有被順利觸發:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    test('should call onChange', () => {
      const onChangeSpy = jest.fn();
      const wrapper = mount(<Demo onChange={onChangeSpy}/>);

      wrapper
        .find('button')
        .props()
        .onClick();

      expect(onChangeSpy).toHaveBeenCalledWith(1);
    });


測試 Class Component,順利的通過了,但到了 function component,遇到以下warning:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    *Warning: An update to Demo inside a test was not wrapped in act(...).
    
        When testing, code that causes React state updates should be wrapped into act(...):
    
        act(() => {
          /* fire events that update state */
        });
        /* assert on the output */*

    *This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
            in Demo (created by WrapperComponent)
            in WrapperComponent*

原本想說算了,如果沒有要繼續測後續的行為,噴個 warning 也不影響測試

但明明是相同行為,為何 function 版有warning?

問題出在 state

act(...) warning 是在提醒你可能有意料之外的事發生

如果是 state 改變是你預期中的,應該用 act() 包起來,否則在這個操作之後,會有意料之外的更新

正常在瀏覽器執行時,react 底層在 callstack 過程中就把這步做掉了

但是單元測試中,是從外部呼叫元件去改變狀態,並沒經過 react 的 callstack

它要你用 act 把 “會改變state的操作” 包起來

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    import { act } from 'react-dom/test-utils';

    ...

    act(() => {
      wrapper
        .find('button')
        .props()
        .onClick();
    });

    ...

That’s it !