Axios catch error Request failed with status code 404

74,862

Solution 1

Testing wrong login URL

The root problem is the test code sets up axios-mock-adapter on a different URL than actually used in Login.vue, so the request is not stubbed:

// login.spec.js:
mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

// Login.vue
axios.post("/login", formData)
            ^^^^^^

The fix is to make the test code use the same URL (i.e., /login):

// login.spec.js
mock.onPost("/login", formData).reply(200, fakeData)

Need to await axios.post()

The unit test isn't awaiting the POST request, so the test wouldn't be able to reliably verify calls or responses (without a hack).

The fix is to update doSigninNormal() to return the axios.post() promise to allow callers to await the result:

// Login.vue
doSigninNormal() {
  return axios.post(...)
}

// login.spec.js
await wrapper.vm.doSigninNormal()
expect(mock.history.post.length).toBe(1)

Verifying login result

To verify the result, you could declare a local data prop to hold the login result 1️⃣, update doSigninNormal() to process the response (which is mocked with fakeData in the test), capturing the result 2️⃣. Then, just check that data property after awaiting doSignInNormal().

// Login.vue
data() {
  return {
    ...
    result: '' 1️⃣
  }
}
methods: {
  doSignInNormal() {
    return axios.post(...)
            .then(resp => this.result = resp.data.result) 2️⃣
  }
}

// login.spec.js
const result = await wrapper.vm.doSigninNormal()
expect(result).toBe(fakeData.result)
expect(wrapper.vm.result).toBe(fakeData.result)

Edit Mocking Axios calls with axios-mock-adapter

Solution 2

 Mocking Axios:

There are two easy ways to mock axios so your tests don't perform real http requests and use a mock object instead:

 set axios as a component property:

import axios from 'axios`;
Vue.component({
  data() {
    return {
      axios,
    }
  },
  methods: {
    makeApiCall() {
      return this.axios.post(...)
    }
  }
})

So you can inject a mock in your tests easily:


it('test axions', function() {
  const post = jest.fn();
  const mock = {
    post,
  }
  // given 
  const wrapper = shallowMount(myComponent, {
    data: {
      axios: mock,
    }
  });

  // when
  wrapper.vm.makeApiCall();

  // then
  expect(post).toHaveBeenCalled();
});

I think this is the most straightforward way.

Use a plugin to inject axios in every component

You can setup a plugin like vue-plugin-axios to inject axios automatically into every component, like:

  makeApiCall(){
    this.$axios.post(...)
  }

Without the need to explicitly declare it in data.

Then in your test, instead of passing the mock as part of data, you pass it as part of mocks, which is the way vue-test-utils deals with global injections:

it('test axions', function() {
  const post = jest.fn();
  const mock = {
    post,
  }
  // given 
  const wrapper = shallowMount(myComponent, {
    mocks: {
      $axios: mock,
    }
  });

  // when
  wrapper.vm.makeApiCall();

  // then
  expect(post).toHaveBeenCalled();
});

This is how to mock axios calls to prevent call real axios and perform real http request.

Configuring mock behavior and access call parameters

With jest.fn you can setup a mock function to return a specific object, like:

const post = jest.fn( () => ({status: 200, response: ...}) )

You can also access the parameters to the call via hasBeenCalledWith' method, or more complex stuff via mock.calls` (more info here):

expect(post).toHaveBeenCalledWith(expectedParams).

So, your final test should be like the following I think:

it('calls axios() with endpoint, method and body',async (done) => {

  // given
  const formData = { email: '[email protected]', password: '111111' };
  const fakeResponse = {response: "fake response"};
  const email = '[email protected]';
  const uri = 'somepath/login/'; // I dont think you can access Vue process env variables in the tests, so you'll need to hardcode.
  const password = '11111';

  const post = jest.fn(() => Promise.resolve({status: 200}) );

  const mock = {
    post,
  }
  const wrapper = shallowMount(Component, {
    data() {
      return {
        axios: mock,
        // email,
        // password, // you could do this instead to write to wrapper.vm later
      }
    }
  });
  wrapper.vm.email = '[email protected]';
  wrapper.vm.password = '111111';

  // when
  await wrapper.vm.doSigninNormal();

  // then
  expect(post).toHaveBeenCalledWith({uri, password, email});

  // or
  const calls = post.mock.calls;
  const firstParam = calls[0][0];
  
  expect(firstParam.uri).toBe(uri);
  expect(firstParam.email).toBe(email);
  expect(firstParam.password).toBe(password);

  done();

});

Solution 3

If axios instance adapter(xhr or http) taked over by axios-mock-adapter, there will have an error with bad baseURL config like this:

{baseURL:'/for/bar'} 

If we send an request like:

get('/api/v1/exampleService')

The last http request will become

'http://host:port/for/bar/for/bar/api/v1/exampleService'

Because mock-adapter take over axios default adapter, apis that not match mock rules will be pass through, and be processed by default adapter , both these adapter select logic go through here(core/dispatchRequest.js):

if (config.baseURL && !isAbsoluteURL(config.url)) { 
   config.url = combineURLs(config.baseURL, config.url);
}

Thus, if you use mock, please use full url starts with http://

Solution 4

The issue is on the axios-mock-adapter package. It requires an instance of axios using the .create() method. See here: creating an instance

In your App.js, use:

import axios from "axios";
const instance = axios.create();

instance.post("http://localhost/api/user/update", {name: "Test"}, {headers: {"Authorization": "Bearer token")}});

Nothing needs to be changed in the tests though.

I got the hint from tests of axios-mock-adapter.

An example of such is: post test

Share:
74,862
priyeshvadhiya
Author by

priyeshvadhiya

I am 25 year-old web developer from INDIA

Updated on July 09, 2022

Comments

  • priyeshvadhiya
    priyeshvadhiya almost 2 years

    I'm testing a login component that uses Axios. I tried mocking Axios with axios-mock-adapter, but when I run the tests, it still errors out with:

    Error: Request failed with status code 404
    

    How do I properly mock Axios in my tests?

    login.spec.js:

    import Vue from 'vue'
    import { shallowMount, createLocalVue } from '@vue/test-utils';
    import Login from '../../src/components/global/login/Login.vue';
    import Raven from "raven-js";
    import jQuery from 'jquery'
    import Vuex from 'vuex'
    import router from '../../src/router'
    var axios = require('axios');
    var MockAdapter = require('axios-mock-adapter');
    
    describe('Login.vue', () => {
      let wrapper;
      let componentInstance;
      let mock;
      beforeEach(() => {
        global.requestAnimationFrame = setImmediate,
        mock = new MockAdapter(axios)
        wrapper = shallowMount(Login, {
          router,
          $: jQuery,
          attachToDocument: true,
          mocks: {
            $t: () => { },
            Raven: Raven,
          },
          data() {
            return {
              email: '',
              password: '',
            }
          }
        })
        componentInstance = wrapper.vm;
      })
    
      afterEach(() => {
        mock.reset()
      })
    
      it('calls `axios()` with `endpoint`, `method` and `body`', async () => {
        const formData = {
          email: '[email protected]',
          password: '111111'
        };
    
        let fakeData = { data: "fake response" }
        mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData);
    
        wrapper.vm.email = '[email protected]';
        wrapper.vm.password = '111111';
        wrapper.vm.doSigninNormal()
      })
    })
    

    Login.vue

    doSigninNormal() {
      const formData = {
        email: this.email,
        password: this.password
      };
      this.$v.$touch()
      if (this.$v.$invalid ) {
        this.loading = false;
        this.emailLostFocus = true;
        this.passwordLostFocus = true;
        $('html, body').animate({scrollTop:110}, 'slow')
    
      } else {
        axios.post("/login", formData, {
          headers: { "X-localization": localStorage.getItem("lan") }
        })
        .then(res => {
          if (!res.data.result) {
            if (res.data.errors) {
              for (var i = 0; i < res.data.errors.length; i++) {
                this.$toaster.error(res.data.errors[i].message);
                if (
                  res.data.errors[0].message == "Your email is not yet verified"
                ) {
                  this.showVerificationLinkButton = true;
                }
                if (res.data.errors[i].field === "email") {
                  this.$toaster.error(res.data.errors[i].message);
                }
                if (res.data.errors[i].field === "password") {
                  this.$toaster.error(res.data.errors[i].message);
                }
              }
            }
    
            this.loading = false;
            this.$v.$reset();
          } else {
            this.loading = false;
            Raven.setUserContext({
              email: res.data.user.email,
              id: res.data.user.id
            });
            this.$store.dispatch("login", res);
            this.$v.$reset();
          }
        })
        .catch((err) => {
           console.log('catch', err);
        });
      }
    }
    
  • priyeshvadhiya
    priyeshvadhiya about 5 years
    i tried so hard and the result like this please see the image
  • tony19
    tony19 about 5 years
    An instance is not actually required. That just happens to be the way their tests are setup.
  • priyeshvadhiya
    priyeshvadhiya about 5 years
    could you give me an example to assign props data and test into my login spec.js?