【问题标题】:How to verify console.log was called in componentDidMount using Jest and Enzyme?如何使用 Jest 和 Enzyme 在 componentDidMount 中验证 console.log 是否被调用?
【发布时间】:2019-07-14 20:50:31
【问题描述】:

我正在尝试测试我的 axios 调用未获得 200 的 HTTP 响应的情况。当 axios 未获得成功的响应时,它会引发错误。在这种情况下,我想验证 console.log 是否被调用了两次。

这是我正在测试的类的 sn-p:

class App extends React.Component {
    ...  
    async componentDidMount() {
        let url = "/api/info/tmp"
        try {
            let response = await axios.get(url);
            ...do stuff
            this.setState(...);
        } catch (e) {
            console.log("Could not get " + url);
            console.log(e);
        }
    }
    ...
}

这是我的玩笑测试的 sn-p

let mockAxios = new MockAdapter(axios);
...
describe("App - componentDidMount() behavior test", () => {
    beforeEach(() => {
        app = shallow(<App />);
    })

    afterEach(() => {
        app = undefined;
        mockAxios.reset();
    });
    ...
    describe("Get " + url + " HTTP response status is not 200", () => {
        beforeAll(() => {
            mockAxios.onGet(url).reply(302, mockData);
        });
        it("Does not set state regardless of response body", () => {
            console.log = jest.fn();
            const state = app.state();
            expect(console.log).toHaveBeenCalledTimes(2);
            expect(state.solutions).toEqual({});
            expect(state.username).toEqual("");
        });
    });
});

我知道console.log = jest.fn() 位正在做某事,因为当我设置它时控制台不再记录虚假错误。但是,测试失败,因为Expected mock function to have been called two times, but it was called zero times.

我尝试将console.log = jest.fn() 移动到“beforeEach”、“beforeAll”中,并作为全局变量。

更新

我很确定这与正在发生的所有异步有关。 如果我这样做:

    it("Does not set state regardless of response body", async () => {
        console.log = jest.fn();
        await app.instance().componentDidMount();
        expect(console.log).toHaveBeenCalledTimes(2);

        const state = app.state();
        expect(state.solutions).toEqual({});
        expect(state.username).toEqual("");
    });

然后测试仍然失败,但我的理由改变了:Expected mock function to have been called two times, but it was called four times. 现在我只是想弄清楚为什么它被调用了四次而不是两次。

更新 2

我知道为什么 console.log 被调用了 4 次!现在我只需要弄清楚我应该如何重构我的测试。 如果我注释掉我的笑话,甚至是整个单元测试

    it("Does not set state regardless of response body", async () => {
        //const state = app.state();
        //expect(state.solutions).toEqual({});
        //expect(state.username).toEqual("");
        //expect(console.log).toHaveBeenCalledTimes(2);
    });

然后我可以在我的控制台中计算出确实已经有两个不同的 console.log 调用。 shallow(&lt;App /&gt;) 必须已经在调用 componentDidMount() 或其他东西。当我添加 app.instance().componentDidMount() 时,我可以直观地看到它正在记录 4 次。

【问题讨论】:

    标签: javascript reactjs jestjs axios enzyme


    【解决方案1】:

    更新答案

    既然您似乎已经知道自己在使用模拟做什么,那么问题可能与 componentDidMount() 有关。

    我相信您对shallow(&lt;App /&gt;) 的调用将已经调用应用程序的componentDidMount() 一次(这意味着您的console.log 将在那里被调用两次)。

    然后,您随后调用app.instance().componentDidMount() - 也就是说,您再次调用componentDidMount()(这意味着您的console.log 将再次被调用两次)。

    所以,总共... 4 次调用 console.log

    希望能指引你正确的方向......

    原答案

    实际上,您的问题与 [this StackOverFlow question on how to "How to mock console when it is used by a third-party library?"

    非常相似

    您可以使用Jest mock functionsspyOn global.console 对象。

    例如,您的测试可能如下所示:

    
    // Setup jest to spy on the console
    const consoleSpy = jest.spyOn(global.console, 'log')
    
    describe('App - componentDidMount() behavior test', () => {
      beforeEach(() => {
        jest.resetAllMocks()  // reset your consoleSpy state back to initial
        app = shallow(<App />)
      })
    
      ...
    
          it('Does not set state regardless of response body', () => {
            const spy = jest.spyOn(global.console, 'log')
            const state = app.state()
            expect(consoleSpy).toHaveBeenCalledTimes(2)
            expect(state.solutions).toEqual({})
            expect(state.username).toEqual('')
          })
      ...
    

    【讨论】:

    • 不幸的是,这不是它。不过我确实做了更新!这与所有正在发生的异步事情有关。
    • @Pickle_Jr 您是否确保每次运行都重置所有模拟?如果您收到Expected mock function to have been called two times, but it was called four times.,那么其他测试运行是否也可能导致console.log 被调用?抱歉...只是在黑暗中拍摄...
    • 别担心,我很感激!我认为这是shallow(&lt;App /&gt;) 所做的事情。我又更新了帖子。
    【解决方案2】:

    理想情况下,您应该将 API 调用移到 componentDidMount 之外并移到它自己的类 method 中。可以通过生命周期方法或事件回调手动调用它。此外,您应该预期响应会以某种方式影响您的 UI state(例如:向用户显示请求失败并重试的消息)。

    下面的例子可以用.then/.catch代替async/await。无论哪种方式,您都在使用Promises,它们是asynchronous,因此他们需要asynchronous 测试。

    注意:以下假设disableLifecycleMethodsenzyme 适配器中为真。此外,仅测试state 更改(或console.log)有点多余;相反,您将测试是否基于当前的state 呈现组件。

    工作示例https://codesandbox.io/s/939w229l9r(包括 end to endintegration 测试 --- 您可以通过单击位于靠近左下角的 Tests 选项卡来运行测试沙盒)


    App.js(这将是一个container,它包含所有相关的state,并根据需要将其分散到其children

    import React, { Component } from 'react';
    
    class App extends Component {
      state = = {
        error: "", 
        isLoading: true,
        solutions: {}, 
        username: ""
      };
    
      componentDidMount() {
        this.fetchData("/api/info/tmp");
      }
    
      fetchData = async (url) => {
        try {
          const res = await axios.get(url);
    
          ...do stuff
    
          this.setState({ 
            error: "", 
            isLoading: false, 
            solutions: res.data.solutions, 
            username: res.data.username 
          });
        } catch (err) {
            this.setState({ 
              error: err, 
              isLoading: false, 
              solutions: {}, 
              username: "" 
            });
        }
      }
    
      render() { ... }
    }
    

    App.test.js(假设您需要 end to end 测试)

    import { shallow } from 'enzyme';
    import App from './App';
    
    const timeout = () =>
      new Promise(resolve => {
        setTimeout(() => {
          resolve();
        }, 2000);
      });
    
    const initialState = {
      error: "", 
      isLoading: true,
      solutions: {}, 
      username: ""
    };
    
    describe("App", () => {
      let wrapper;
      beforeEach(() => {
        wrapper = shallow(<App />);
        wrapper.setState({ ...initialState });
      });
    
      afterAll(() => {
         wrapper.unmount();
      });
    
      it("sets data to state based upon successful API call", async () => { 
       wrapper.instance().fetchData("/api/info/tmp");
       await timeout();
    
       wrapper.update();
       expect(wrapper.state('isLoading')).toBeFalsy();
       expect(wrapper.state('solutions')).toEqual({ somedata });
       expect(wrapper.state('username')).toEqual("Some User");
      });
    
      it("displays an error upon unsuccessful API call", async () => { 
       wrapper.instance().fetchData("/api/bad/url");
       await timeout();
    
       wrapper.update();
       expect(wrapper.state('isLoading')).toBeFalsy();
       expect(wrapper.state('solutions')).toEqual({});
       expect(wrapper.state('username')).toEqual("");
       expect(wrapper.state('error')).toEqual("No data found.");
      });
    });
    

    App.test.js(假设您需要 integration 测试)

    import axios from "axios";
    import MockAdapter from "axios-mock-adapter";
    import React from "react";
    import { shallow } from "enzyme";
    import App from "../App";
    
    const solutions = [{ ... }, { ... }];
    const username = "Some User"
    
    const mockAxios = new MockAdapter(axios);
    
    const initialState = {
      error: "", 
      isLoading: true,
      solutions: {}, 
      username: ""
    };
    
    describe("App", () => { 
      let wrapper;
      beforeEach(() => {
        wrapper = shallow(<App />);
        wrapper.setState({ ...initialState });
      });
    
      afterEach(() => {
        mock.reset();
      });
    
      afterAll(() => {
        mock.restore();
        wrapper.unmount();
      });
    
      it("displays an error upon unsuccessful API call", async () => { 
        try {
           mockAxios.onGet("/users").networkErrorOnce();
    
           await axios.get("users");
    
         } catch (err) {
           const error = err.toString();
           wrapper.setState({ 
             error, 
             isLoading: false,
             solutions: {}, 
             username: ""
           });
    
           wrapper.update();
           expect(wrapper.state('isLoading')).toBeEqual(error);
           expect(wrapper.state('isLoading')).toBeFalsy();
           expect(wrapper.state('solutions')).toEqual({});
           expect(wrapper.state('username')).toEqual("");
         } 
      });
    
      it("sets data to state based upon successful API call", async () => {
        try {
          mockAxios.onGet("/users").reply(200, { solutions, username });
    
          const res = await axios.get("users");
    
          wrapper.setState({ 
            error: "", 
            isLoading: true,
            solutions: res.data.solutions, 
            username: res.data.username
          });
    
          wrapper.update();
          expect(wrapper.state('isLoading')).toBeFalsy();
          expect(wrapper.state('solutions')).toEqual(solutions);
          expect(wrapper.state('username')).toEqual(username);
        } catch (e) {
            console.log(e);
        } 
      });
    });
    

    【讨论】:

      【解决方案3】:

      我想通了!有点...我不确定 为什么 它像这样工作,但是在实际的“它”中设置模拟不起作用。 解决方案是制作beforeEachafterEach

      describe("Get " + url + " HTTP response status is not 200", () => {
          beforeAll(() => {
              mockAxios.onGet(url).reply(302, mockData);
          });
          beforeEach(() => {
              console.log = jest.fn();
          });
      
          afterEach(() => {
              jest.resetAllMocks();
          });
          it("Does not set state regardless of response body", async () => {
              const state = app.state();
              expect(state.solutions).toEqual({});
              expect(state.username).toEqual("");
              expect(console.log).toHaveBeenCalledTimes(2);
          });
      });
      

      【讨论】:

        猜你喜欢
        • 2021-04-14
        • 2022-01-18
        • 2018-12-23
        • 2018-10-26
        • 2020-10-15
        • 2019-03-18
        • 2020-03-28
        • 2018-08-31
        • 1970-01-01
        相关资源
        最近更新 更多