Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Axios catch error Request failed with status code 404

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);
    });
  }
}
like image 663
priyeshvadhiya Avatar asked Mar 25 '19 13:03

priyeshvadhiya


Video Answer


2 Answers

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

like image 113
tony19 Avatar answered Sep 25 '22 15:09

tony19


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

});
like image 29
Sergeon Avatar answered Sep 23 '22 15:09

Sergeon