剩余模块单元测试
defaults 模块单元测试
defaults
模块为请求配置提供了一些默认的属性和方法,我们需要为其编写单元测试。
test/defaults.spec.ts
:
import axios, { AxiosTransformer } from '../src/index'
import { getAjaxRequest } from './helper'
import { deepMerge } from '../src/helpers/util'
describe('defaults', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should transform request json', () => {
expect((axios.defaults.transformRequest as AxiosTransformer[])[0]({ foo: 'bar' })).toBe('{"foo":"bar"}')
})
test('should do nothing to request string', () => {
expect((axios.defaults.transformRequest as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
})
test('should transform response json', () => {
const data = (axios.defaults.transformResponse as AxiosTransformer[])[0]('{"foo":"bar"}')
expect(typeof data).toBe('object')
expect(data.foo).toBe('bar')
})
test('should do nothing to response string', () => {
expect((axios.defaults.transformResponse as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
})
test('should use global defaults config', () => {
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
})
})
test('should use modified defaults config', () => {
axios.defaults.baseURL = 'http://example.com/'
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://example.com/foo')
delete axios.defaults.baseURL
})
})
test('should use request config', () => {
axios('/foo', {
baseURL: 'http://www.example.com'
})
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://www.example.com/foo')
})
})
test('should use default config for custom instance', () => {
const instance = axios.create({
xsrfCookieName: 'CUSTOM-XSRF-TOKEN',
xsrfHeaderName: 'X-CUSTOM-XSRF-TOKEN'
})
document.cookie = instance.defaults.xsrfCookieName + '=foobarbaz'
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[instance.defaults.xsrfHeaderName!]).toBe('foobarbaz')
document.cookie =
instance.defaults.xsrfCookieName +
'=;expires=' +
new Date(Date.now() - 86400000).toUTCString()
})
})
test('should use GET headers', () => {
axios.defaults.headers.get['X-CUSTOM-HEADER'] = 'foo'
axios.get('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
delete axios.defaults.headers.get['X-CUSTOM-HEADER']
})
})
test('should use POST headers', () => {
axios.defaults.headers.post['X-CUSTOM-HEADER'] = 'foo'
axios.post('/foo', {})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
delete axios.defaults.headers.post['X-CUSTOM-HEADER']
})
})
test('should use header config', () => {
const instance = axios.create({
headers: {
common: {
'X-COMMON-HEADER': 'commonHeaderValue'
},
get: {
'X-GET-HEADER': 'getHeaderValue'
},
post: {
'X-POST-HEADER': 'postHeaderValue'
}
}
})
instance.get('/foo', {
headers: {
'X-FOO-HEADER': 'fooHeaderValue',
'X-BAR-HEADER': 'barHeaderValue'
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders).toEqual(
deepMerge(axios.defaults.headers.common, axios.defaults.headers.get, {
'X-COMMON-HEADER': 'commonHeaderValue',
'X-GET-HEADER': 'getHeaderValue',
'X-FOO-HEADER': 'fooHeaderValue',
'X-BAR-HEADER': 'barHeaderValue'
})
)
})
})
test('should be used by custom instance if set before instance created', () => {
axios.defaults.baseURL = 'http://example.org/'
const instance = axios.create()
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://example.org/foo')
delete axios.defaults.baseURL
})
})
test('should not be used by custom instance if set after instance created', () => {
const instance = axios.create()
axios.defaults.baseURL = 'http://example.org/'
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
})
})
})
import axios, { AxiosTransformer } from '../src/index'
import { getAjaxRequest } from './helper'
import { deepMerge } from '../src/helpers/util'
describe('defaults', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should transform request json', () => {
expect((axios.defaults.transformRequest as AxiosTransformer[])[0]({ foo: 'bar' })).toBe('{"foo":"bar"}')
})
test('should do nothing to request string', () => {
expect((axios.defaults.transformRequest as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
})
test('should transform response json', () => {
const data = (axios.defaults.transformResponse as AxiosTransformer[])[0]('{"foo":"bar"}')
expect(typeof data).toBe('object')
expect(data.foo).toBe('bar')
})
test('should do nothing to response string', () => {
expect((axios.defaults.transformResponse as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
})
test('should use global defaults config', () => {
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
})
})
test('should use modified defaults config', () => {
axios.defaults.baseURL = 'http://example.com/'
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://example.com/foo')
delete axios.defaults.baseURL
})
})
test('should use request config', () => {
axios('/foo', {
baseURL: 'http://www.example.com'
})
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://www.example.com/foo')
})
})
test('should use default config for custom instance', () => {
const instance = axios.create({
xsrfCookieName: 'CUSTOM-XSRF-TOKEN',
xsrfHeaderName: 'X-CUSTOM-XSRF-TOKEN'
})
document.cookie = instance.defaults.xsrfCookieName + '=foobarbaz'
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[instance.defaults.xsrfHeaderName!]).toBe('foobarbaz')
document.cookie =
instance.defaults.xsrfCookieName +
'=;expires=' +
new Date(Date.now() - 86400000).toUTCString()
})
})
test('should use GET headers', () => {
axios.defaults.headers.get['X-CUSTOM-HEADER'] = 'foo'
axios.get('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
delete axios.defaults.headers.get['X-CUSTOM-HEADER']
})
})
test('should use POST headers', () => {
axios.defaults.headers.post['X-CUSTOM-HEADER'] = 'foo'
axios.post('/foo', {})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
delete axios.defaults.headers.post['X-CUSTOM-HEADER']
})
})
test('should use header config', () => {
const instance = axios.create({
headers: {
common: {
'X-COMMON-HEADER': 'commonHeaderValue'
},
get: {
'X-GET-HEADER': 'getHeaderValue'
},
post: {
'X-POST-HEADER': 'postHeaderValue'
}
}
})
instance.get('/foo', {
headers: {
'X-FOO-HEADER': 'fooHeaderValue',
'X-BAR-HEADER': 'barHeaderValue'
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders).toEqual(
deepMerge(axios.defaults.headers.common, axios.defaults.headers.get, {
'X-COMMON-HEADER': 'commonHeaderValue',
'X-GET-HEADER': 'getHeaderValue',
'X-FOO-HEADER': 'fooHeaderValue',
'X-BAR-HEADER': 'barHeaderValue'
})
)
})
})
test('should be used by custom instance if set before instance created', () => {
axios.defaults.baseURL = 'http://example.org/'
const instance = axios.create()
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('http://example.org/foo')
delete axios.defaults.baseURL
})
})
test('should not be used by custom instance if set after instance created', () => {
const instance = axios.create()
axios.defaults.baseURL = 'http://example.org/'
instance.get('/foo')
return getAjaxRequest().then(request => {
expect(request.url).toBe('/foo')
})
})
})
transform 模块单元测试
transform
模块用来定义请求和响应的转换方法,我们需要为其编写单元测试。
import axios, { AxiosResponse, AxiosTransformer } from '../src/index'
import { getAjaxRequest } from './helper'
describe('transform', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should transform JSON to string', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data)
return getAjaxRequest().then(request => {
expect(request.params).toBe('{"foo":"bar"}')
})
})
test('should transform string to JSON', done => {
let response: AxiosResponse
axios('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: '{"foo": "bar"}'
})
setTimeout(() => {
expect(typeof response.data).toBe('object')
expect(response.data.foo).toBe('bar')
done()
}, 100)
})
})
test('should override default transform', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data, {
transformRequest(data) {
return data
}
})
return getAjaxRequest().then(request => {
expect(request.params).toEqual({ foo: 'bar' })
})
})
test('should allow an Array of transformers', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data, {
transformRequest: (axios.defaults.transformRequest as AxiosTransformer[]).concat(function(
data
) {
return data.replace('bar', 'baz')
})
})
return getAjaxRequest().then(request => {
expect(request.params).toBe('{"foo":"baz"}')
})
})
test('should allowing mutating headers', () => {
const token = Math.floor(Math.random() * Math.pow(2, 64)).toString(36)
axios('/foo', {
transformRequest: (data, headers) => {
headers['X-Authorization'] = token
return data
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-Authorization']).toEqual(token)
})
})
})
import axios, { AxiosResponse, AxiosTransformer } from '../src/index'
import { getAjaxRequest } from './helper'
describe('transform', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should transform JSON to string', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data)
return getAjaxRequest().then(request => {
expect(request.params).toBe('{"foo":"bar"}')
})
})
test('should transform string to JSON', done => {
let response: AxiosResponse
axios('/foo').then(res => {
response = res
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: '{"foo": "bar"}'
})
setTimeout(() => {
expect(typeof response.data).toBe('object')
expect(response.data.foo).toBe('bar')
done()
}, 100)
})
})
test('should override default transform', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data, {
transformRequest(data) {
return data
}
})
return getAjaxRequest().then(request => {
expect(request.params).toEqual({ foo: 'bar' })
})
})
test('should allow an Array of transformers', () => {
const data = {
foo: 'bar'
}
axios.post('/foo', data, {
transformRequest: (axios.defaults.transformRequest as AxiosTransformer[]).concat(function(
data
) {
return data.replace('bar', 'baz')
})
})
return getAjaxRequest().then(request => {
expect(request.params).toBe('{"foo":"baz"}')
})
})
test('should allowing mutating headers', () => {
const token = Math.floor(Math.random() * Math.pow(2, 64)).toString(36)
axios('/foo', {
transformRequest: (data, headers) => {
headers['X-Authorization'] = token
return data
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['X-Authorization']).toEqual(token)
})
})
})
xsrf 模块单元测试
xsrf
模块提供了一套防御 xsrf
攻击的解决方案,我们需要为其编写单元测试。
test/xsrf.spec.ts
:
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('xsrf', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
document.cookie =
axios.defaults.xsrfCookieName + '=;expires=' + new Date(Date.now() - 86400000).toUTCString()
})
test('should not set xsrf header if cookie is null', () => {
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
})
})
test('should set xsrf header if cookie is set', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
})
})
test('should not set xsrf header for cross origin', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('http://example.com/')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
})
})
test('should set xsrf header for cross origin when using withCredentials', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('http://example.com/', {
withCredentials: true
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
})
})
})
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('xsrf', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
document.cookie =
axios.defaults.xsrfCookieName + '=;expires=' + new Date(Date.now() - 86400000).toUTCString()
})
test('should not set xsrf header if cookie is null', () => {
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
})
})
test('should set xsrf header if cookie is set', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('/foo')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
})
})
test('should not set xsrf header for cross origin', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('http://example.com/')
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
})
})
test('should set xsrf header for cross origin when using withCredentials', () => {
document.cookie = axios.defaults.xsrfCookieName + '=12345'
axios('http://example.com/', {
withCredentials: true
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
})
})
})
注意在 afterEach
函数中我们清空了 xsrf
相关的 cookie。
上传下载模块单元测试
上传下载模块允许我们监听上传和下载的进度,我们需要为其编写单元测试。
test/progress.spec.ts
:
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('progress', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should add a download progress handler', () => {
const progressSpy = jest.fn()
axios('/foo', { onDownloadProgress: progressSpy })
return getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: '{"foo": "bar"}'
})
expect(progressSpy).toHaveBeenCalled()
})
})
test('should add a upload progress handler', () => {
const progressSpy = jest.fn()
axios('/foo', { onUploadProgress: progressSpy })
return getAjaxRequest().then(request => {
// Jasmine AJAX doesn't trigger upload events.Waiting for jest-ajax fix
// expect(progressSpy).toHaveBeenCalled()
})
})
})
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('progress', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should add a download progress handler', () => {
const progressSpy = jest.fn()
axios('/foo', { onDownloadProgress: progressSpy })
return getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: '{"foo": "bar"}'
})
expect(progressSpy).toHaveBeenCalled()
})
})
test('should add a upload progress handler', () => {
const progressSpy = jest.fn()
axios('/foo', { onUploadProgress: progressSpy })
return getAjaxRequest().then(request => {
// Jasmine AJAX doesn't trigger upload events.Waiting for jest-ajax fix
// expect(progressSpy).toHaveBeenCalled()
})
})
})
注意,由于 jasmine-ajax
插件不会派发 upload
事件,这个未来可以通过我们自己编写的 jest-ajax
插件来解决,目前不写断言的情况它会直接通过。
HTTP 授权模块单元测试
HTTP 授权模块为我们在请求头中添加 Authorization
字段,我们需要为其编写单元测试。
test/auth.spec.ts
:
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('auth', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should accept HTTP Basic auth with username/password', () => {
axios('/foo', {
auth: {
username: 'Aladdin',
password: 'open sesame'
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['Authorization']).toBe('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
})
})
test('should fail to encode HTTP Basic auth credentials with non-Latin1 characters', () => {
return axios('/foo', {
auth: {
username: 'Aladßç£☃din',
password: 'open sesame'
}
})
.then(() => {
throw new Error(
'Should not succeed to make a HTTP Basic auth request with non-latin1 chars in credentials.'
)
})
.catch(error => {
expect(/character/i.test(error.message)).toBeTruthy()
})
})
})
import axios from '../src/index'
import { getAjaxRequest } from './helper'
describe('auth', () => {
beforeEach(() => {
jasmine.Ajax.install()
})
afterEach(() => {
jasmine.Ajax.uninstall()
})
test('should accept HTTP Basic auth with username/password', () => {
axios('/foo', {
auth: {
username: 'Aladdin',
password: 'open sesame'
}
})
return getAjaxRequest().then(request => {
expect(request.requestHeaders['Authorization']).toBe('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
})
})
test('should fail to encode HTTP Basic auth credentials with non-Latin1 characters', () => {
return axios('/foo', {
auth: {
username: 'Aladßç£☃din',
password: 'open sesame'
}
})
.then(() => {
throw new Error(
'Should not succeed to make a HTTP Basic auth request with non-latin1 chars in credentials.'
)
})
.catch(error => {
expect(/character/i.test(error.message)).toBeTruthy()
})
})
})
静态方法模块单元测试
静态方法模块为 axios
对象添加了 2 个静态方法,我们需要为其编写单元测试。
test/static.spec.ts
:
import axios from '../src/index'
describe('promise', () => {
test('should support all', done => {
let fulfilled = false
axios.all([true, false]).then(arg => {
fulfilled = arg[0]
})
setTimeout(() => {
expect(fulfilled).toBeTruthy()
done()
}, 100)
})
test('should support spread', done => {
let sum = 0
let fulfilled = false
let result: any
axios
.all([123, 456])
.then(
axios.spread((a, b) => {
sum = a + b
fulfilled = true
return 'hello world'
})
)
.then(res => {
result = res
})
setTimeout(() => {
expect(fulfilled).toBeTruthy()
expect(sum).toBe(123 + 456)
expect(result).toBe('hello world')
done()
}, 100)
})
})
import axios from '../src/index'
describe('promise', () => {
test('should support all', done => {
let fulfilled = false
axios.all([true, false]).then(arg => {
fulfilled = arg[0]
})
setTimeout(() => {
expect(fulfilled).toBeTruthy()
done()
}, 100)
})
test('should support spread', done => {
let sum = 0
let fulfilled = false
let result: any
axios
.all([123, 456])
.then(
axios.spread((a, b) => {
sum = a + b
fulfilled = true
return 'hello world'
})
)
.then(res => {
result = res
})
setTimeout(() => {
expect(fulfilled).toBeTruthy()
expect(sum).toBe(123 + 456)
expect(result).toBe('hello world')
done()
}, 100)
})
})
补充未覆盖的代码测试
我们发现,跑完测试后,仍有一些代码没有覆盖到测试,其中 core/xhr.ts
文件的第 43 行:
if (responseType) {
request.responseType = responseType
}
if (responseType) {
request.responseType = responseType
}
我们并未在测试中设置过 responseType
,因此我们在 test/requests.spect.ts
文件中补充相关测试:
test('should support array buffer response', done => {
let response: AxiosResponse
function str2ab(str: string) {
const buff = new ArrayBuffer(str.length * 2)
const view = new Uint16Array(buff)
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i)
}
return buff
}
axios('/foo', {
responseType: 'arraybuffer'
}).then(data => {
response = data
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
// @ts-ignore
response: str2ab('Hello world')
})
setTimeout(() => {
expect(response.data.byteLength).toBe(22)
done()
}, 100)
})
})
test('should support array buffer response', done => {
let response: AxiosResponse
function str2ab(str: string) {
const buff = new ArrayBuffer(str.length * 2)
const view = new Uint16Array(buff)
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i)
}
return buff
}
axios('/foo', {
responseType: 'arraybuffer'
}).then(data => {
response = data
})
getAjaxRequest().then(request => {
request.respondWith({
status: 200,
// @ts-ignore
response: str2ab('Hello world')
})
setTimeout(() => {
expect(response.data.byteLength).toBe(22)
done()
}, 100)
})
})
另外我们发现 core/xhr.ts
文件的第 13 行:
method = 'get'
method = 'get'
分支没有测试完全。因为实际上代码执行到这的时候 method
是一定会有的,所以我们不必为其指定默认值,另外还需要在 method!.toUpperCase()
的时候使用非空断言。
同时core/xhr.ts
文件的第 66 行:
const responseData = responseType !== 'text' ? request.response : request.responseText
const responseData = responseType !== 'text' ? request.response : request.responseText
分支也没有测试完全。这里我们应该先判断存在 responseType
存在的情况下再去和 text
做对比,需要修改逻辑:
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
这样再次跑测试,就覆盖了所有的分支。
到此为止,除了我们之前说的 helpers/error.ts
模块中对于 super
的测试的分支覆盖率没达到 100%,其它模块均达到 100% 的测试覆盖率。
有些有强迫症的同学可能会觉得,能不能通过某种手段让它的覆盖率达到 100% 呢,这里其实有一个奇技淫巧,在 helpers/error.ts
文件的 constructor
函数上方加一个 /* istanbul ignore next */
注释,这样其实相当于忽略了整个构造函数的测试,这样我们就可以达到 100% 的覆盖率了。
/* istanbul ignore next */
在我们去阅读一些开源代码的时候经常会遇到,主要用途就是用来忽略测试用的,这个技巧不可滥用,除非你明确的知道这段代码不需要测试,否则你不应该使用它。滥用就失去了单元测试的意义了。
至此,我们就完成了整个 ts-axios
库的测试了,我们也成功地让测试覆盖率达到目标 99% 以上。下一章我会教大家如果打包构建和发布我们的 ts-axios
库。