### 彪哥

爱好前端

- 初识 TypeScript

- TypeScript 常用语法

- ts-axios 项目初始化

- ts-axios 基础功能实现

- ts-axios 异常情况处理

- ts-axios 接口扩展

- ts-axios 拦截器实现

- ts-axios 配置化实现

- ts-axios 取消功能实现

- ts-axios 更多功能实现

- ts-axios 单元测试

  - 前言
  - Jest 安装和配置
  - 辅助模块单元测试
  - 请求模块单元测试
  - headers 模块单元测试
  - Axios 实例模块单元测试
  - 拦截器模块单元测试
  - mergeConfig 模块单元测试
  - 请求取消模块单元测试
  - 剩余模块单元测试
    - defaults 模块单元测试
    - transform 模块单元测试
    - xsrf 模块单元测试
    - 上传下载模块单元测试
    - HTTP 授权模块单元测试
    - 静态方法模块单元测试
    - 补充未覆盖的代码测试

- ts-axios 部署与发布

- 《TypeScript 从零实现 axios》
- ts-axios 单元测试

HuangYi
2020-01-05
目录

# 剩余模块单元测试

# # 剩余模块单元测试

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

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

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

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

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

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

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

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

1
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'

1

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

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

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

1

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

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

1

这样再次跑测试，就覆盖了所有的分支。

到此为止，除了我们之前说的 `helpers/error.ts` 模块中对于 `super` 的测试的分支覆盖率没达到 100%，其它模块均达到 100% 的测试覆盖率。

有些有强迫症的同学可能会觉得，能不能通过某种手段让它的覆盖率达到 100% 呢，这里其实有一个奇技淫巧，在 `helpers/error.ts` 文件的 `constructor` 函数上方加一个 `/* istanbul ignore next */` 注释，这样其实相当于忽略了整个构造函数的测试，这样我们就可以达到 100% 的覆盖率了。

`/* istanbul ignore next */` 在我们去阅读一些开源代码的时候经常会遇到，主要用途就是用来忽略测试用的，这个技巧不可滥用，除非你明确的知道这段代码不需要测试，否则你不应该使用它。滥用就失去了单元测试的意义了。

至此，我们就完成了整个 `ts-axios` 库的测试了，我们也成功地让测试覆盖率达到目标 99% 以上。下一章我会教大家如果打包构建和发布我们的 `ts-axios` 库。

编辑
(opens new window)
#TypeScript
上次更新: 2022/12/14, 19:36:42
请求取消模块单元测试
ts-axios 编译与发布

← 请求取消模块单元测试 ts-axios 编译与发布→

最近更新

  01
Git修改分支名
08-11

  02
CSS给table的tbody添加滚动条
06-29

  03
我做了一个手写春联小网页，祝大家虎年暴富 原创
01-28

更多文章>

Copyright © 2016-2022 Hesb | MIT License

- 跟随系统
- 浅色模式
- 深色模式
- 阅读模式