Skip to main content

[TS] ts-axios(24) - 单元测试(剩余模块)


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')
})
})
})

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)
})
})
})

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')
})
})
})

注意在 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()
})
})
})

注意,由于 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()
})
})
})

静态方法模块单元测试

静态方法模块为 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)
})
})

补充未覆盖的代码测试

我们发现,跑完测试后,仍有一些代码没有覆盖到测试,其中 core/xhr.ts 文件的第 43 行:

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)
})
})

另外我们发现 core/xhr.ts 文件的第 13 行:

method = 'get'

分支没有测试完全。因为实际上代码执行到这的时候 method 是一定会有的,所以我们不必为其指定默认值,另外还需要在 method!.toUpperCase() 的时候使用非空断言。

同时core/xhr.ts 文件的第 66 行:

const responseData = responseType !== 'text' ? request.response : request.responseText

分支也没有测试完全。这里我们应该先判断存在 responseType 存在的情况下再去和 text 做对比,需要修改逻辑:

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 库。