Jest安裝配置
版本升級
{
"@types/jest": "^24.0.13",
"jest": "^24.8.0",
"jest-config": "^24.8.0",
"ts-jest": "^24.0.2",
"typescript": "^3.4.5"
}
配置
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "jsdom",
"testRegex": "/test/.*\\.(test|spec)\\.(ts)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": [
"src/*.{js,ts}",
"ser/**/*.{js,ts}"
],
"setupFileAfterEnv": [
"<rootDir>/test/boot.ts"
]
},
輔助模塊單元測試
util 模塊測試
// ./test/helpers/util.spec.ts
import {
isDate,
isPlainObject,
isFormData,
isURLSearchParams,
extend,
deepMerge
} from '../../src/helpers/util'
describe('helpers:util', () => {
describe('isXX', () => {
test('should validate Date', () => {
expect(isDate(new Date())).toBeTruthy()
expect(isDate(Date.now())).toBeFalsy()
})
test('should validate PlainObject', () => {
expect(isPlainObject({})).toBeTruthy()
expect(isPlainObject(new Date())).toBeFalsy()
})
test('should validate FormData', () => {
expect(isFormData(new FormData())).toBeTruthy()
expect(isFormData({})).toBeFalsy()
})
test('should validate URLSearchParams', () => {
expect(isURLSearchParams(new URLSearchParams())).toBeTruthy()
expect(isURLSearchParams('foo=1&bar=2')).toBeFalsy()
})
})
describe('extend', () => {
test('should be mutable', () => {
const a = Object.create(null)
const b = { foo: 123 }
extend(a, b)
expect(a.foo).toBe(123)
})
test('should extend properties', function() {
const a = { foo: 123, bar: 456 }
const b = { bar: 789 }
const c = extend(a, b)
expect(c.foo).toBe(123)
expect(c.bar).toBe(789)
})
})
describe('deepMerge', () => {
test('should be immutable', () => {
const a = Object.create(null)
const b: any = { foo: 123 }
const c: any = { bar: 456 }
deepMerge(a, b, c)
expect(typeof a.foo).toBe('undefined')
expect(typeof a.bar).toBe('undefined')
expect(typeof b.bar).toBe('undefined')
expect(typeof c.foo).toBe('undefined')
})
test('should deepMerge properties', () => {
const a = { foo: 123 }
const b = { bar: 456 }
const c = { foo: 789 }
const d = deepMerge(a, b, c)
expect(d.foo).toBe(789)
expect(d.bar).toBe(456)
})
test('should deepMerge recursively', function() {
const a = { foo: { bar: 123 } }
const b = { foo: { baz: 456 }, bar: { qux: 789 } }
const c = deepMerge(a, b)
expect(c).toEqual({
foo: {
bar: 123,
baz: 456
},
bar: {
qux: 789
}
})
})
test('should remove all references from nested objects', () => {
const a = { foo: { bar: 123 } }
const b = {}
const c = deepMerge(a, b)
expect(c).toEqual({
foo: {
bar: 123
}
})
expect(c.foo).not.toBe(a.foo)
})
test('should handle null and undefined arguments', () => {
expect(deepMerge(undefined, undefined)).toEqual({})
expect(deepMerge(undefined, { foo: 123 })).toEqual({ foo: 123 })
expect(deepMerge({ foo: 123 }, undefined)).toEqual({ foo: 123 })
expect(deepMerge(null, null)).toEqual({})
expect(deepMerge(null, { foo: 123 })).toEqual({ foo: 123 })
expect(deepMerge({ foo: 123 }, null)).toEqual({ foo: 123 })
})
})
})
cookie模塊測試
// test/helpers/cookie.spec.ts
import cookie from '../../src/helpers/cookie'
describe('helpers:cookie', () => {
test('should read cookies', () => {
document.cookie = 'foo=baz'
expect(cookie.read('foo')).toBe('baz')
})
test('should return null if cookie name is not exist', () => {
document.cookie = 'foo=baz'
expect(cookie.read('bar')).toBeNull()
})
})
data模塊測試
// ./test/helpers/data.spec.ts
import { transformRequest, transformResponse } from '../../src/helpers/data'
describe('helpers:data', () => {
describe('transformRequest', () => {
test('should transform request data to string if data is a PlainObject', () => {
const a = { a: 1 }
expect(transformRequest(a)).toBe('{"a":1}')
})
test('should do nothing if data is not a PlainObject', () => {
const a = new URLSearchParams('a=b')
expect(transformRequest(a)).toBe(a)
})
})
describe('transformResponse', () => {
test('should transform response data to Object if data is a JSON string', () => {
const a = '{"a": 2}'
expect(transformResponse(a)).toEqual({ a: 2 })
})
test('should do nothing if data is a string but not a JSON string', () => {
const a = '{a: 2}'
expect(transformResponse(a)).toBe('{a: 2}')
})
test('should do nothing if data is not a string', () => {
const a = { a: 2 }
expect(transformResponse(a)).toBe(a)
})
})
})
error模塊測試
// test/helpers/error.spec.ts
import { createError } from '../../src/helpers/error'
import { AxiosRequestConfig, AxiosResponse } from '../../src/types'
describe('helpers::error', function() {
test('should create an Error with message, config, code, request, response and isAxiosError', () => {
const request = new XMLHttpRequest()
const config: AxiosRequestConfig = { method: 'post' }
const response: AxiosResponse = {
status: 200,
statusText: 'OK',
headers: null,
request,
config,
data: { foo: 'bar' }
}
const error = createError('Boom!', config, 'SOMETHING', request, response)
expect(error instanceof Error).toBeTruthy()
expect(error.message).toBe('Boom!')
expect(error.config).toBe(config)
expect(error.code).toBe('SOMETHING')
expect(error.request).toBe(request)
expect(error.response).toBe(response)
expect(error.isAxiosError).toBeTruthy()
})
})
headers模塊測試
// ./test/helpers/headers.spec.ts
import { parseHeaders, processHeaders, flattenHeaders } from '../../src/helpers/headers'
describe('helpers:header', () => {
describe('parseHeaders', () => {
test('should parse headers', () => {
const parsed = parseHeaders(
'Content-Type: application/json\r\n' +
'Connection: keep-alive\r\n' +
'Transfer-Encoding: chunked\r\n' +
'Date: Tue, 21 May 2019 09:23:44 GMT\r\n' +
':aa\r\n' +
'key:'
)
expect(parsed['content-type']).toBe('application/json')
expect(parsed['connection']).toBe('keep-alive')
expect(parsed['transfer-encoding']).toBe('chunked')
expect(parsed['date']).toBe('Tue, 21 May 2019 09:23:44 GMT')
expect(parsed['key']).toBe('')
})
test('should return empty object if headers is empty string', () => {
expect(parseHeaders('')).toEqual({})
})
})
describe('processHeaders', () => {
test('should normalize Content-Type header name', () => {
const headers: any = {
'conTenT-Type': 'foo/bar',
'Content-length': 1024
}
processHeaders(headers, {})
expect(headers['Content-Type']).toBe('foo/bar')
expect(headers['conTenT-Type']).toBeUndefined()
expect(headers['Content-length']).toBe(1024)
})
test('should set Content-Type if not set and data is PlainObject', () => {
const headers: any = {}
processHeaders(headers, { a: 1 })
expect(headers['Content-Type']).toBe('application/json;charset=utf-8')
})
test('should set not Content-Type if not set and data is not PlainObject', () => {
const headers: any = {}
processHeaders(headers, new URLSearchParams('a=b'))
expect(headers['Content-Type']).toBeUndefined()
})
test('should do nothing if headers is undefined or null', () => {
expect(processHeaders(undefined, {})).toBeUndefined()
expect(processHeaders(null, {})).toBeNull()
})
})
describe('flattenHeaders', () => {
test('should flatten the headers and include common headers', () => {
const headers = {
Accept: 'application/json',
common: {
'X-COMMON-HEADER': 'commonHeaderValue'
},
get: {
'X-GET-HEADER': 'getHeaderValue'
},
post: {
'X-POST-HEADER': 'postHeaderValue'
}
}
expect(flattenHeaders(headers, 'get')).toEqual({
Accept: 'application/json',
'X-COMMON-HEADER': 'commonHeaderValue',
'X-GET-HEADER': 'getHeaderValue'
})
})
test('should flatten the headers without common headers', () => {
const headers = {
Accept: 'application/json',
get: {
'X-GET-HEADER': 'getHeaderValue'
}
}
expect(flattenHeaders(headers, 'patch')).toEqual({
Accept: 'application/json'
})
})
test('should do nothing if headers is undefined or null', () => {
expect(flattenHeaders(undefined, 'get')).toBeUndefined()
expect(flattenHeaders(null, 'post')).toBeNull()
})
})
})
解決出現的bug
// ./src/helpers/headers.ts
//...
export function parseHeaders(headers: string): any {
let parsed = Object.create(null)
if (!headers) {
return parsed
}
headers.split('\r\n').forEach(line => {
let [key, ...vals] = line.split(':')
key = key.trim().toLowerCase()
if (!key) {
return
}
let val = vals.join(':').trim()
parsed[key] = val
})
return parsed
}
url模塊測試
// ./test/helpers/url.spec.ts
import { buildURL, isAbsoluteURL, combineURL, isURLSameOrigin } from '../../src/helpers/url'
describe('helpers:url', () => {
describe('buildURL', () => {
test('should support null params', () => {
expect(buildURL('/foo')).toBe('/foo')
})
test('should support params', () => {
expect(
buildURL('/foo', {
foo: 'bar'
})
).toBe('/foo?foo=bar')
})
test('should ignore if some param value is null', () => {
expect(
buildURL('/foo', {
foo: 'bar',
baz: null
})
).toBe('/foo?foo=bar')
})
test('should ignore if the only param value is null', () => {
expect(
buildURL('/foo', {
baz: null
})
).toBe('/foo')
})
test('should support object params', () => {
expect(
buildURL('/foo', {
foo: {
bar: 'baz'
}
})
).toBe('/foo?foo=' + encodeURI('{"bar":"baz"}'))
})
test('should support date params', () => {
const date = new Date()
expect(
buildURL('/foo', {
date: date
})
).toBe('/foo?date=' + date.toISOString())
})
test('should support array params', () => {
expect(
buildURL('/foo', {
foo: ['bar', 'baz']
})
).toBe('/foo?foo[]=bar&foo[]=baz')
})
test('should support special char params', () => {
expect(
buildURL('/foo', {
foo: '@:$, '
})
).toBe('/foo?foo=@:$,+')
})
test('should support existing params', () => {
expect(
buildURL('/foo?foo=bar', {
bar: 'baz'
})
).toBe('/foo?foo=bar&bar=baz')
})
test('should correct discard url hash mark', () => {
expect(
buildURL('/foo?foo=bar#hash', {
query: 'baz'
})
).toBe('/foo?foo=bar&query=baz')
})
test('should use serializer if provided', () => {
const serializer = jest.fn(() => {
return 'foo=bar'
})
const params = { foo: 'bar' }
expect(buildURL('/foo', params, serializer)).toBe('/foo?foo=bar')
expect(serializer).toHaveBeenCalled()
expect(serializer).toHaveBeenCalledWith(params)
})
test('should support URLSearchParams', () => {
expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toBe('/foo?bar=baz')
})
})
describe('isAbsoluteURL', () => {
test('should return true if URL begins with valid scheme name', () => {
expect(isAbsoluteURL('https://api.github.com/users')).toBeTruthy()
expect(isAbsoluteURL('custom-scheme-v1.0://example.com/')).toBeTruthy()
expect(isAbsoluteURL('HTTP://example.com/')).toBeTruthy()
})
test('should return false if URL begins with invalid scheme name', () => {
expect(isAbsoluteURL('123://example.com/')).toBeFalsy()
expect(isAbsoluteURL('!valid://example.com/')).toBeFalsy()
})
test('should return true if URL is protocol-relative', () => {
expect(isAbsoluteURL('//example.com/')).toBeTruthy()
})
test('should return false if URL is relative', () => {
expect(isAbsoluteURL('/foo')).toBeFalsy()
expect(isAbsoluteURL('foo')).toBeFalsy()
})
})
describe('combineURL', () => {
test('should combine URL', () => {
expect(combineURL('https://api.github.com', '/users')).toBe('https://api.github.com/users')
})
test('should remove duplicate slashes', () => {
expect(combineURL('https://api.github.com/', '/users')).toBe('https://api.github.com/users')
})
test('should insert missing slash', () => {
expect(combineURL('https://api.github.com', 'users')).toBe('https://api.github.com/users')
})
test('should not insert slash when relative url missing/empty', () => {
expect(combineURL('https://api.github.com/users', '')).toBe('https://api.github.com/users')
})
test('should allow a single slash for relative url', () => {
expect(combineURL('https://api.github.com/users', '/')).toBe('https://api.github.com/users/')
})
})
describe('isURLSameOrigin', () => {
test('should detect same origin', () => {
expect(isURLSameOrigin(window.location.href)).toBeTruthy()
})
test('should detect different origin', () => {
expect(isURLSameOrigin('https://github.com/axios/axios')).toBeFalsy()
})
})
})
bug修復
// ./src/helpers/url.ts
export function isAbsoluteURL(url: string): boolean {
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
}
請求模塊單元測試
依賴安裝
npm i jasmine-ajax
npm i @types/jasmine-ajax
// ./test/boot.ts
const JasmineCore = require('jasmine-core')
// @ts-ignore
global.getJasmineRequireObj = function() {
return JasmineCore
}
require('jasmine-ajax')
測試代碼
// ./test/request/spec.ts
import axios, { AxiosResponse, AxiosError } from '../src/index'
import { getAjaxRequest } from './helper'
describe('requests', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should treat single string arg as url', () => {
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
expect(request.method).toBe('GET')
})
})
test('should treat method value as lowercase string', done => {
axios({
url: '/foo',
method: 'POST'
}).then(response => {
expect(response.config.method).toBe('post')
done()
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200
})
})
})
test('should reject on network errors', done => {
const resolveSpy = jest.fn((res: AxiosResponse) => {
return res
})
const rejectSpy = jest.fn((e: AxiosError) => {
return e
})
jasmine.Ajax.uninstall()
axios('/foo')
.then(resolveSpy)
.catch(rejectSpy)
.then(next)
function next(reason: AxiosResponse | AxiosError) {
expect(resolveSpy).not.toHaveBeenCalled()
expect(rejectSpy).toHaveBeenCalled()
expect(reason instanceof Error).toBeTruthy()
expect((reason as AxiosError).message).toBe('Network Error')
expect(reason.request).toEqual(expect.any(XMLHttpRequest))
jasmine.Ajax.install()
done()
}
})
test('should reject when request timeout', done => {
let err: AxiosError
axios('/foo', {
timeout: 2000,
method: 'post'
}).catch(error => {
err = error
})
getAjaxRequest().then(request => {
// @ts-ignore
request.eventBus.trigger('timeout')
setTimeout(() => {
expect(err instanceof Error).toBeTruthy()
expect(err.message).toBe('Timeout of 2000 ms exceeded')
done()
}, 100)
})
})
test('should reject when validateStatus returns false', done => {
const resolveSpy = jest.fn((res: AxiosResponse) => {
return res
})
const rejectSpy = jest.fn((e: AxiosError) => {
return e
})
axios('/foo', {
validateStatus(status) {
return status !== 500
}
})
.then(resolveSpy)
.catch(rejectSpy)
.then(next)
getAjaxRequest().then(request => {
request.respondWith({
status: 500
})
})
function next(reason: AxiosError | AxiosResponse) {
expect(resolveSpy).not.toHaveBeenCalled()
expect(rejectSpy).toHaveBeenCalled()
expect(reason instanceof Error).toBeTruthy()
expect((reason as AxiosError).message).toBe('Request failed with status code 500')
expect((reason as AxiosError).response!.status).toBe(500)
done()
}
})
test('should resolve when validateStatus returns true', done => {
const resolveSpy = jest.fn((res: AxiosResponse) => {
return res
})
const rejectSpy = jest.fn((e: AxiosError) => {
return e
})
axios('/foo', {
validateStatus(status) {
return status === 500
}
})
.then(resolveSpy)
.catch(rejectSpy)
.then(next)
getAjaxRequest().then(request => {
request.respondWith({
status: 500
})
})
function next(res: AxiosResponse | AxiosError) {
expect(resolveSpy).toHaveBeenCalled()
expect(rejectSpy).not.toHaveBeenCalled()
expect(res.config.url).toBe('/foo')
done()
}
})
test('should return JSON when resolved', done => {
let response: AxiosResponse
axios('/api/account/signup', {
auth: {
username: '',
password: ''
},
method: 'post',
headers: {
Accept: 'application/json'
}
}).then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
statusText: 'OK',
responseText: '{"a": 1}'
})
setTimeout(() => {
expect(response.data).toEqual({ a: 1 })
done()
}, 100)
})
})
test('should return JSON when rejecting', done => {
let response: AxiosResponse
axios('/api/account/signup', {
auth: {
username: '',
password: ''
},
method: 'post',
headers: {
Accept: 'application/json'
}
}).catch(error => {
response = error.response
})
getAjaxRequest().then(request => {
request.respondWith({
status: 400,
statusText: 'Bad Request',
responseText: '{"error": "BAD USERNAME", "code": 1}'
})
setTimeout(() => {
expect(typeof response.data).toBe('object')
expect(response.data.error).toBe('BAD USERNAME')
expect(response.data.code).toBe(1)
done()
}, 100)
})
})
test('should supply correct response', done => {
let response: AxiosResponse
axios.post('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
statusText: 'OK',
responseText: '{"foo": "bar"}',
responseHeaders: {
'Content-Type': 'application/json'
}
})
setTimeout(() => {
expect(response.data.foo).toBe('bar')
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.headers['content-type']).toBe('application/json')
done()
}, 100)
})
})
test('should allow overriding Content-Type header case-insensitive', () => {
let response: AxiosResponse
axios
.post(
'/foo',
{ prop: 'value' },
{
headers: {
'content-type': 'application/json'
}
}
)
.then(res => {
response = res
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['Content-Type']).toBe('application/json')
})
})
})
bug修復
// ./core/Axios.ts
request(url: any, config?: any): AxiosPromise {
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
config = mergeConfig(this.defaults, config)
config.method = config.method.toLowerCase()
// ...
}
// ./core/dispatchRequest.ts
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
throwIfCancellationRequested(config)
processConfig(config)
return xhr(config).then(
res => {
return transformResponseData(res)
},
e => {
if (e && e.response) {
e.response = transformResponseData(e.response)
}
return Promise.reject(e)
}
)
}
headers模塊單元測試
// ./test/headers.spec.ts
import axios from '../src/index'
import { getAjaxRequest } from './helper'
function testHeaderValue(headers: any, key: string, val?: string): void {
let found = false
for (let k in headers) {
if (k.toLowerCase() === key.toLowerCase()) {
found = true
expect(headers[k]).toBe(val)
break
}
}
if (!found) {
if (typeof val === 'undefined') {
expect(headers.hasOwnProperty(key)).toBeFalsy()
} else {
throw new Error(key + ' was not found in headers')
}
}
}
describe('headers', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should use default common headers', () => {
const headers = axios.defaults.headers.common
axios('/foo')
return getAjaxRequest().then(request => {
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
expect(request.requestHeaders[key]).toEqual(headers[key])
}
}
})
})
test('should add extra headers for post', () => {
axios.post('/foo', 'fizz=buzz')
return getAjaxRequest().then(request => {
testHeaderValue(request.requestHeaders, 'Content-Type', 'application/x-www-form-urlencoded')
})
})
test('should use application/json when posting an object', () => {
axios.post('/foo/bar', {
firstName: 'foo',
lastName: 'bar'
})
return getAjaxRequest().then(request => {
testHeaderValue(request.requestHeaders, 'Content-Type', 'application/json;charset=utf-8')
})
})
test('should remove content-type if data is empty', () => {
axios.post('/foo')
return getAjaxRequest().then(request => {
testHeaderValue(request.requestHeaders, 'Content-Type', undefined)
})
})
it('should preserve content-type if data is false', () => {
axios.post('/foo', false)
return getAjaxRequest().then(request => {
testHeaderValue(request.requestHeaders, 'Content-Type', 'application/x-www-form-urlencoded')
})
})
test('should remove content-type if data is FormData', () => {
const data = new FormData()
data.append('foo', 'bar')
axios.post('/foo', data)
return getAjaxRequest().then(request => {
testHeaderValue(request.requestHeaders, 'Content-Type', undefined)
})
})
})
Axios 實例模塊單元測試
// ./test/instance.spec.ts
import axios, { AxiosRequestConfig, AxiosResponse } from '../src/index'
import { getAjaxRequest } from './helper'
describe('instance', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should make a http request without verb helper', () => {
const instance = axios.create()
instance('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
})
})
test('should make a http request', () => {
const instance = axios.create()
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
expect(request.method).toBe('GET')
})
})
test('should make a post request', () => {
const instance = axios.create()
instance.post('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('POST')
})
})
test('should make a put request', () => {
const instance = axios.create()
instance.put('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('PUT')
})
})
test('should make a patch request', () => {
const instance = axios.create()
instance.patch('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('PATCH')
})
})
test('should make a options request', () => {
const instance = axios.create()
instance.options('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('OPTIONS')
})
})
test('should make a delete request', () => {
const instance = axios.create()
instance.delete('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('DELETE')
})
})
test('should make a head request', () => {
const instance = axios.create()
instance.head('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('HEAD')
})
})
test('should use instance options', () => {
const instance = axios.create({ timeout: 1000 })
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.timeout).toBe(1000)
})
})
test('should have defaults.headers', () => {
const instance = axios.create({ baseURL: 'https://api.example.com' })
expect(typeof instance.defaults.headers).toBe('object')
expect(typeof instance.defaults.headers.common).toBe('object')
})
test('should have interceptors on the instance', done => {
axios.interceptors.request.use(config => {
config.timeout = 2000
return config
})
const instance = axios.create()
instance.interceptors.request.use(config => {
config.withCredentials = true
return config
})
let response: AxiosResponse
instance.get('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200
})
setTimeout(() => {
expect(response.config.timeout).toEqual(0)
expect(response.config.withCredentials).toEqual(true)
done()
}, 100)
})
})
test('should get the computed uri', () => {
const fakeConfig: AxiosRequestConfig = {
baseURL: 'https://www.baidu.com/',
url: '/user/12345',
params: {
idClient: 1,
idTest: 2,
testString: 'thisIsATest'
}
}
expect(axios.getUri(fakeConfig)).toBe(
'https://www.baidu.com/user/12345?idClient=1&idTest=2&testString=thisIsATest'
)
})
})
攔截器模塊
// ./test/interceptor.spec.ts
import axios, { AxiosRequestConfig, AxiosResponse } from '../src/index'
import { getAjaxRequest } from './helper'
describe('interceptors', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should add a request interceptor', () => {
const instance = axios.create()
instance.interceptors.request.use((config: AxiosRequestConfig) => {
config.headers.test = 'added by interceptor'
return config
})
instance('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders.test).toBe('added by interceptor')
})
})
test('should add a request interceptor that returns a new config object', () => {
const instance = axios.create()
instance.interceptors.request.use(() => {
return {
url: '/bar',
method: 'post'
}
})
instance('/foo')
return getAjaxRequest().then(request => {
expect(request.method).toBe('POST')
expect(request.url).toBe('/bar')
})
})
test('should add a request interceptor that returns a promise', done => {
const instance = axios.create()
instance.interceptors.request.use((config: AxiosRequestConfig) => {
return new Promise(resolve => {
setTimeout(() => {
config.headers.async = 'promise'
resolve(config)
}, 10)
})
})
instance('/foo')
setTimeout(() => {
getAjaxRequest().then(request => {
expect(request.requestHeaders.async).toBe('promise')
done()
})
}, 100)
})
test('should add multiple request interceptors', () => {
const instance = axios.create()
instance.interceptors.request.use(config => {
config.headers.test1 = '1'
return config
})
instance.interceptors.request.use(config => {
config.headers.test2 = '2'
return config
})
instance.interceptors.request.use(config => {
config.headers.test3 = '3'
return config
})
instance('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders.test1).toBe('1')
expect(request.requestHeaders.test2).toBe('2')
expect(request.requestHeaders.test3).toBe('3')
})
})
test('should add a response interceptor', done => {
let response: AxiosResponse
const instance = axios.create()
instance.interceptors.response.use(data => {
data.data = data.data + ' - modified by interceptor'
return data
})
instance('/foo').then(data => {
response = data
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
setTimeout(() => {
expect(response.data).toBe('OK - modified by interceptor')
done()
}, 100)
})
})
test('should add a response interceptor that returns a new data object', done => {
let response: AxiosResponse
const instance = axios.create()
instance.interceptors.response.use(() => {
return {
data: 'stuff',
headers: null,
status: 500,
statusText: 'ERR',
request: null,
config: {}
}
})
instance('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
setTimeout(() => {
expect(response.data).toBe('stuff')
expect(response.headers).toBeNull()
expect(response.status).toBe(500)
expect(response.statusText).toBe('ERR')
expect(response.request).toBeNull()
expect(response.config).toEqual({})
done()
}, 100)
})
})
test('should add a response interceptor that returns a promise', done => {
let response: AxiosResponse
const instance = axios.create()
instance.interceptors.response.use(data => {
return new Promise(resolve => {
// do something async
setTimeout(() => {
data.data = 'you have been promised!'
resolve(data)
}, 10)
})
})
instance('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
setTimeout(() => {
expect(response.data).toBe('you have been promised!')
done()
}, 100)
})
})
test('should add multiple response interceptors', done => {
let response: AxiosResponse
const instance = axios.create()
instance.interceptors.response.use(data => {
data.data = data.data + '1'
return data
})
instance.interceptors.response.use(data => {
data.data = data.data + '2'
return data
})
instance.interceptors.response.use(data => {
data.data = data.data + '3'
return data
})
instance('/foo').then(data => {
response = data
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
setTimeout(() => {
expect(response.data).toBe('OK123')
done()
}, 100)
})
})
test('should allow removing interceptors', done => {
let response: AxiosResponse
let intercept
const instance = axios.create()
instance.interceptors.response.use(data => {
data.data = data.data + '1'
return data
})
intercept = instance.interceptors.response.use(data => {
data.data = data.data + '2'
return data
})
instance.interceptors.response.use(data => {
data.data = data.data + '3'
return data
})
instance.interceptors.response.eject(intercept)
instance.interceptors.response.eject(5)
instance('/foo').then(data => {
response = data
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
setTimeout(() => {
expect(response.data).toBe('OK13')
done()
}, 100)
})
})
})
./core/xhr.ts
const {
//...
headers = {},
//...
} = config
mergeConfig 模塊單元測試
// ./test/mergeConfig.spec.ts
import axios from '../src/index'
import mergeConfig from '../src/core/mergeConfig'
describe('mergeConfig', () => {
const defaults = axios.defaults
test('should accept undefined for second argument', () => {
expect(mergeConfig(defaults, undefined)).toEqual(defaults)
})
test('should accept an object for second argument', () => {
expect(mergeConfig(defaults, {})).toEqual(defaults)
})
test('should not leave references', () => {
const merged = mergeConfig(defaults, {})
expect(merged).not.toBe(defaults)
expect(merged.headers).not.toBe(defaults.headers)
})
test('should allow setting request options', () => {
const config = {
url: '__sample url__',
params: '__sample params__',
data: { foo: true }
}
const merged = mergeConfig(defaults, config)
expect(merged.url).toBe(config.url)
expect(merged.params).toBe(config.params)
expect(merged.data).toEqual(config.data)
})
test('should not inherit request options', () => {
const localDefaults = {
url: '__sample url__',
params: '__sample params__',
data: { foo: true }
}
const merged = mergeConfig(localDefaults, {})
expect(merged.url).toBeUndefined()
expect(merged.params).toBeUndefined()
expect(merged.data).toBeUndefined()
})
test('should return default headers if pass config2 with undefined', () => {
expect(
mergeConfig(
{
headers: 'x-mock-header'
},
undefined
)
).toEqual({
headers: 'x-mock-header'
})
})
test('should merge auth, headers with defaults', () => {
expect(
mergeConfig(
{
auth: undefined
},
{
auth: {
username: 'foo',
password: 'test'
}
}
)
).toEqual({
auth: {
username: 'foo',
password: 'test'
}
})
expect(
mergeConfig(
{
auth: {
username: 'foo',
password: 'test'
}
},
{
auth: {
username: 'baz',
password: 'foobar'
}
}
)
).toEqual({
auth: {
username: 'baz',
password: 'foobar'
}
})
})
test('should overwrite auth, headers with a non-object value', () => {
expect(
mergeConfig(
{
headers: {
common: {
Accept: 'application/json, text/plain, */*'
}
}
},
{
headers: null
}
)
).toEqual({
headers: null
})
})
test('should allow setting other options', () => {
const merged = mergeConfig(defaults, {
timeout: 123
})
expect(merged.timeout).toBe(123)
})
})
優化
// ./src/core/mergeConfig.ts
//...
function deepMergeStrat(val1: any, val2: any): any {
if (isPlainObject(val2)) {
return deepMerge(val1, val2)
} else if (typeof val2 !== 'undefined') {
return val2
} else if (isPlainObject(val1)) {
return deepMerge(val1)
} else {
return val1
}
}
請求取消模塊單元測試
// ./test/cancel/Cancel.spec.ts
import Cancel, { isCancel } from '../../src/cancel/Cancel'
describe('cancel:Cancel', () => {
test('should returns correct result when message is specified', () => {
const cancel = new Cancel('Operation has been canceled.')
expect(cancel.message).toBe('Operation has been canceled.')
})
test('should returns true if value is a Cancel', () => {
expect(isCancel(new Cancel())).toBeTruthy()
})
test('should returns false if value is not a Cancel', () => {
expect(isCancel({ foo: 'bar' })).toBeFalsy()
})
})
// ./test/cancel/CancelToken.spec.ts
import CancelToken from '../../src/cancel/CancelToken'
import Cancel from '../../src/cancel/Cancel'
import { Canceler } from '../../src/types'
describe('CancelToken', () => {
describe('reason', () => {
test('should returns a Cancel if cancellation has been requested', () => {
let cancel: Canceler
let token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
expect(token.reason).toEqual(expect.any(Cancel))
expect(token.reason!.message).toBe('Operation has been canceled.')
})
test('should has no side effect if call cancellation for multi times', () => {
let cancel: Canceler
let token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
cancel!('Operation has been canceled.')
expect(token.reason).toEqual(expect.any(Cancel))
expect(token.reason!.message).toBe('Operation has been canceled.')
})
test('should returns undefined if cancellation has not been requested', () => {
const token = new CancelToken(() => {
// do nothing
})
expect(token.reason).toBeUndefined()
})
})
describe('promise', () => {
test('should returns a Promise that resolves when cancellation is requested', done => {
let cancel: Canceler
const token = new CancelToken(c => {
cancel = c
})
token.promise.then(value => {
expect(value).toEqual(expect.any(Cancel))
expect(value.message).toBe('Operation has been canceled.')
done()
})
cancel!('Operation has been canceled.')
})
})
describe('throwIfRequested', () => {
test('should throws if cancellation has been requested', () => {
let cancel: Canceler
const token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
try {
token.throwIfRequested()
fail('Expected throwIfRequested to throw.')
} catch (thrown) {
if (!(thrown instanceof Cancel)) {
fail('Expected throwIfRequested to throw a Cancel, but test threw ' + thrown + '.')
}
expect(thrown.message).toBe('Operation has been canceled.')
}
})
test('should does not throw if cancellation has not been requested', () => {
const token = new CancelToken(() => {
// do nothing
})
token.throwIfRequested()
})
})
describe('source', () => {
test('should returns an object containing token and cancel function', () => {
const source = CancelToken.source()
expect(source.token).toEqual(expect.any(CancelToken))
expect(source.cancel).toEqual(expect.any(Function))
expect(source.token.reason).toBeUndefined()
source.cancel('Operation has been canceled.')
expect(source.token.reason).toEqual(expect.any(Cancel))
expect(source.token.reason!.message).toBe('Operation has been canceled.')
})
})
})
// ./test/cancel.spec.ts
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('cancel', () => {
const CancelToken = axios.CancelToken
const Cancel = axios.Cancel
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
describe('when called before sending request', () => {
test('should rejects Promise with a Cancel object', () => {
const source = CancelToken.source()
source.cancel('Operation has been canceled.')
return axios
.get('/foo', {
cancelToken: source.token
})
.catch(reason => {
expect(reason).toEqual(expect.any(Cancel))
expect(reason.message).toBe('Operation has been canceled.')
})
})
})
describe('when called after request has been sent', () => {
test('should rejects Promise with a Cancel object', done => {
const source = CancelToken.source()
axios
.get('/foo/bar', {
cancelToken: source.token
})
.catch(reason => {
expect(reason).toEqual(expect.any(Cancel))
expect(reason.message).toBe('Operation has been canceled.')
done()
})
getAjaxRequest().then(request => {
source.cancel('Operation has been canceled.')
setTimeout(() => {
request.respondWith({
status: 200,
responseText: 'OK'
})
}, 100)
})
})
test('calls abort on request object', done => {
const source = CancelToken.source()
let request: any
axios
.get('/foo/bar', {
cancelToken: source.token
})
.catch(() => {
expect(request.statusText).toBe('abort')
done()
})
getAjaxRequest().then(req => {
source.cancel()
request = req
})
})
})
describe('when called after response has been received', () => {
test('should not cause unhandled rejection', done => {
const source = CancelToken.source()
axios
.get('/foo', {
cancelToken: source.token
})
.then(() => {
window.addEventListener('unhandledrejection', () => {
done.fail('Unhandled rejection.')
})
source.cancel()
setTimeout(done, 100)
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
})
})
})
})