剩余模块单元测试
# 剩余模块单元测试
# 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')
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 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)
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 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')
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
注意在 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()
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
注意,由于 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()
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 静态方法模块单元测试
静态方法模块为 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)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 补充未覆盖的代码测试
我们发现,跑完测试后,仍有一些代码没有覆盖到测试,其中 core/xhr.ts
文件的第 43 行:
if (responseType) {
request.responseType = responseType
}
2
3
我们并未在测试中设置过 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)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
另外我们发现 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
库。