0

I'm trying test for cases when my axios call does not get an HTTP response of 200. When axios does not get a successful response, it throws an error. I want to verify that console.log gets called twice in this case.

Here's a snippet of the class I'm testing:

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);
        }
    }
    ...
}

And here's a snippet of my jest test

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("");
        });
    });
});

I know the console.log = jest.fn() bit is doing something because the console does not log the fake error anymore when I set it. However, the test fails because Expected mock function to have been called two times, but it was called zero times.

I've tried moving the console.log = jest.fn() into the "beforeEach", "beforeAll", and as a global variable.

UPDATE

I am pretty sure it's something to do with all the async that is going on. If I do this:

    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("");
    });

Then the test still fails but my reason changed: Expected mock function to have been called two times, but it was called four times. Now I just got to figure out why it was called four times not twice.

UPDATE 2

I figured out why console.log was being called 4 times! Now I just need to figure out how I should refactor my tests. If I comment out my jest mock, and even the whole unit test

    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);
    });

Then I can count in my console that there are already indeed two different console.log calls. shallow(<App />) must be already calling componentDidMount() or something. When I add app.instance().componentDidMount(), I can visually see that it is logging 4 times.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Pickle_Jr
  • 85
  • 8

3 Answers3

3

Updated Answer

Since it looks like you already know what you're doing with mocks, perhaps the issue has to do with componentDidMount().

I believe that your call to shallow(<App />) will already call App's componentDidMount() one time (which means your console.log will get called twice there).

Then, you subsequently call app.instance().componentDidMount() - that is, you call componentDidMount() again (which means your console.log will get called twice there again).

So, total... 4 calls to console.log.

Hope that points you in the right direction...

Original Answer

Actually, your question looks quite similar to [this StackOverFlow question on how to "How to mock console when it is used by a third-party library?"

You can use Jest mock functions to spyOn the global.console object.

For example, your test may look like this:


// 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('')
      })
  ...
Alvin S. Lee
  • 4,984
  • 30
  • 34
  • Unfortunately this is not it. I did make an update though! It's something to do with all the asynchronous stuff going on. – Pickle_Jr Feb 21 '19 at 03:48
  • @Pickle_Jr Are you making sure that you are resetting all of your mocks with each run? If you're getting `Expected mock function to have been called two times, but it was called four times.` then is it possible that other test runs are also leading to `console.log` getting called? Sorry... just a shot in the dark... – Alvin S. Lee Feb 21 '19 at 03:57
  • No worries, I appreciate it! I think it's something `shallow()` did. I updated my post again. – Pickle_Jr Feb 21 '19 at 04:10
1

Ideally, you'd move your API call outside of componentDidMount and into its own class method. Thay way it can be manually invoked from a lifecycle method or from an event callback. Also, you should anticipate the response to affect your UI state in some fashion (example: displaying a message to the user that the request failed and to try again).

The following example can be done with .then/.catch instead of async/await. Either way, you're working with Promises that are asynchronous and therefore they need asynchronous tests.

Note: The below assumes disableLifecycleMethods is true in the enzyme adapter. Also, just testing state changes (or a console.log) is a bit superfluous; instead, you would test if a component is rendered based upon the current state.

Working example: https://codesandbox.io/s/939w229l9r (includes both end to end and integration tests --- you can run the tests by clicking on the Tests tab located near the bottom left of the sandbox)


App.js (this will be a container that holds all relevant state and disperses it to its children as needed)

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 (this assumes you'd want an end to end test)

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 (this assumes you'd want an integration test)

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);
    } 
  });
});
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51
0

I figured it out! Kind of... I am not certain why it works like this, but setting the mock in the actual "it" did not work. The solution was making a beforeEach and afterEach

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);
    });
});
Pickle_Jr
  • 85
  • 8