axios 捕获错误请求失败,状态码为 404
Axios catch error Request failed with status code 404
我正在测试使用 Axios 的登录组件。我尝试用 axios-mock-adapter
模拟 Axios,但是当我 运行 测试时,它仍然出错:
Error: Request failed with status code 404
如何在我的测试中正确模拟 Axios?
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: 'example@gmail.com',
password: '111111'
};
let fakeData = { data: "fake response" }
mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData);
wrapper.vm.email = 'example@gmail.com';
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);
});
}
}
模拟 Axios:
有两种模拟 axios 的简单方法,这样您的测试就不会执行真正的 http 请求,而是使用模拟对象:
将 axios 设置为组件 属性:
import axios from 'axios`;
Vue.component({
data() {
return {
axios,
}
},
methods: {
makeApiCall() {
return this.axios.post(...)
}
}
})
因此您可以轻松地在测试中注入模拟:
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();
});
我认为这是最直接的方法。
使用插件在每个组件中注入 axios
你可以设置一个像 vue-plugin-axios 这样的插件来自动将 axios 注入到每个组件中,比如:
makeApiCall(){
this.$axios.post(...)
}
无需在data
中显式声明。
然后在您的测试中,不是将模拟作为 data
的一部分传递,而是将其作为 mocks
的一部分传递,这是 vue-test-utils
处理全局注入的方式:
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();
});
这是模拟 axios 调用以防止调用真正的 axios 并执行真正的 http 请求的方法。
配置模拟行为和访问调用参数
使用 jest.fn
您可以为 return 特定对象设置模拟函数,例如:
const post = jest.fn( () => ({status: 200, response: ...}) )
您还可以通过 hasBeenCalledWith' method, or more complex stuff via
mock.calls` (more info here):
访问调用参数
expect(post).toHaveBeenCalledWith(expectedParams)
.
所以,我认为你的最终测试应该像下面这样:
it('calls axios() with endpoint, method and body',async (done) => {
// given
const formData = { email: 'example@gmail.com', password: '111111' };
const fakeResponse = {response: "fake response"};
const email = 'example@gmail.com';
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 = 'example@gmail.com';
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();
});
问题出在 axios-mock-adapter 包上。它需要使用 .create()
方法的 axios 实例。
看这里:
creating an instance
在你的App.js中,
使用:
import axios from "axios";
const instance = axios.create();
instance.post("http://localhost/api/user/update", {name: "Test"}, {headers: {"Authorization": "Bearer token")}});
尽管在测试中不需要更改任何内容。
我从 axios-mock-adapter 的测试中得到提示。
例如:
post test
测试登录错误URL
根本问题是测试代码在 axios-mock-adapter
上设置的 URL 与 Login.vue
中实际使用的不同 URL,因此请求未被存根:
// login.spec.js:
mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Login.vue
axios.post("/login", formData)
^^^^^^
解决方法是使测试代码使用相同的 URL(即 /login
):
// login.spec.js
mock.onPost("/login", formData).reply(200, fakeData)
需要等待axios.post()
单元测试未等待 POST
请求,因此测试将无法可靠地验证调用或响应(如果没有 hack)。
修复方法是将 doSigninNormal()
更新为 return axios.post()
允许调用方等待结果的承诺:
// Login.vue
doSigninNormal() {
return axios.post(...)
}
// login.spec.js
await wrapper.vm.doSigninNormal()
expect(mock.history.post.length).toBe(1)
正在验证登录结果
为了验证结果,你可以声明一个本地数据 prop 来保存登录结果 1️⃣,更新 doSigninNormal()
来处理响应(在测试中用 fakeData
模拟),捕获结果2️⃣。然后,在等待 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)
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'}
如果我们发送这样的请求:
get('/api/v1/exampleService')
最后一个http请求会变成
'http://host:port/for/bar/for/bar/api/v1/exampleService'
因为mock-adapter接管了axios默认的adapter,不符合mock规则的api会通过,由default adapter处理,这两个adapterselect逻辑都从这里走([=23= .js):
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}
因此,如果您使用 mock,请使用完整的 url 以 http://
开头
我正在测试使用 Axios 的登录组件。我尝试用 axios-mock-adapter
模拟 Axios,但是当我 运行 测试时,它仍然出错:
Error: Request failed with status code 404
如何在我的测试中正确模拟 Axios?
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: 'example@gmail.com',
password: '111111'
};
let fakeData = { data: "fake response" }
mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData);
wrapper.vm.email = 'example@gmail.com';
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);
});
}
}
模拟 Axios:
有两种模拟 axios 的简单方法,这样您的测试就不会执行真正的 http 请求,而是使用模拟对象:
将 axios 设置为组件 属性:
import axios from 'axios`;
Vue.component({
data() {
return {
axios,
}
},
methods: {
makeApiCall() {
return this.axios.post(...)
}
}
})
因此您可以轻松地在测试中注入模拟:
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();
});
我认为这是最直接的方法。
使用插件在每个组件中注入 axios
你可以设置一个像 vue-plugin-axios 这样的插件来自动将 axios 注入到每个组件中,比如:
makeApiCall(){
this.$axios.post(...)
}
无需在data
中显式声明。
然后在您的测试中,不是将模拟作为 data
的一部分传递,而是将其作为 mocks
的一部分传递,这是 vue-test-utils
处理全局注入的方式:
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();
});
这是模拟 axios 调用以防止调用真正的 axios 并执行真正的 http 请求的方法。
配置模拟行为和访问调用参数
使用 jest.fn
您可以为 return 特定对象设置模拟函数,例如:
const post = jest.fn( () => ({status: 200, response: ...}) )
您还可以通过 hasBeenCalledWith' method, or more complex stuff via
mock.calls` (more info here):
expect(post).toHaveBeenCalledWith(expectedParams)
.
所以,我认为你的最终测试应该像下面这样:
it('calls axios() with endpoint, method and body',async (done) => {
// given
const formData = { email: 'example@gmail.com', password: '111111' };
const fakeResponse = {response: "fake response"};
const email = 'example@gmail.com';
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 = 'example@gmail.com';
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();
});
问题出在 axios-mock-adapter 包上。它需要使用 .create()
方法的 axios 实例。
看这里:
creating an instance
在你的App.js中, 使用:
import axios from "axios";
const instance = axios.create();
instance.post("http://localhost/api/user/update", {name: "Test"}, {headers: {"Authorization": "Bearer token")}});
尽管在测试中不需要更改任何内容。
我从 axios-mock-adapter 的测试中得到提示。
例如: post test
测试登录错误URL
根本问题是测试代码在 axios-mock-adapter
上设置的 URL 与 Login.vue
中实际使用的不同 URL,因此请求未被存根:
// login.spec.js:
mock.onPost(`${process.env.VUE_APP_BASE_URL}/login/`, formData).reply(200, fakeData)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Login.vue
axios.post("/login", formData)
^^^^^^
解决方法是使测试代码使用相同的 URL(即 /login
):
// login.spec.js
mock.onPost("/login", formData).reply(200, fakeData)
需要等待axios.post()
单元测试未等待 POST
请求,因此测试将无法可靠地验证调用或响应(如果没有 hack)。
修复方法是将 doSigninNormal()
更新为 return axios.post()
允许调用方等待结果的承诺:
// Login.vue
doSigninNormal() {
return axios.post(...)
}
// login.spec.js
await wrapper.vm.doSigninNormal()
expect(mock.history.post.length).toBe(1)
正在验证登录结果
为了验证结果,你可以声明一个本地数据 prop 来保存登录结果 1️⃣,更新 doSigninNormal()
来处理响应(在测试中用 fakeData
模拟),捕获结果2️⃣。然后,在等待 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)
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'}
如果我们发送这样的请求:
get('/api/v1/exampleService')
最后一个http请求会变成
'http://host:port/for/bar/for/bar/api/v1/exampleService'
因为mock-adapter接管了axios默认的adapter,不符合mock规则的api会通过,由default adapter处理,这两个adapterselect逻辑都从这里走([=23= .js):
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}
因此,如果您使用 mock,请使用完整的 url 以 http://
开头