### 彪哥

爱好前端

- 初识 TypeScript

- TypeScript 常用语法

- ts-axios 项目初始化

- ts-axios 基础功能实现

- ts-axios 异常情况处理

- ts-axios 接口扩展

- ts-axios 拦截器实现

- ts-axios 配置化实现

- ts-axios 取消功能实现

- ts-axios 更多功能实现

- ts-axios 单元测试

  - 前言
  - Jest 安装和配置
  - 辅助模块单元测试
  - 请求模块单元测试
    - jasmine-ajax
    - 测试代码编写
  - headers 模块单元测试
  - Axios 实例模块单元测试
  - 拦截器模块单元测试
  - mergeConfig 模块单元测试
  - 请求取消模块单元测试
  - 剩余模块单元测试

- ts-axios 部署与发布

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

HuangYi
2020-01-05
目录

# 请求模块单元测试

# # 请求模块单元测试

请求模块是 axios 最基础的模块，通过一个 axios 方法发送 Ajax 请求。

## # jasmine-ajax

Jasmine
(opens new window) 是一个 BDD(行为驱动开发)的测试框架，它有很多成熟的插件，比如我们要用到的 `jasmine-ajax`
(opens new window)，它会为我们发出的 Ajax 请求根据规范定义一组假的响应，并跟踪我们发出的Ajax请求，可以让我们方便的为结果做断言。

其实 Jest 也可以去写插件，但并没有现成的 Ajax 相关的 Jest 插件，但是 Jest 测试中我们仍然可以使用 Jasmine 相关的插件，只需要做一些小小的配置即可。

当然，未来我也会考虑去编写一个 Ajax 相关的 Jest 插件，目前我们仍然使用 `jasmine-ajax` 去配合我们编写测试。

`jasmine-ajax` 依赖 `jasmine-core`，因此首先我们要安装几个依赖包，`jasmine-ajax`、`jasmine-core` 和 `@types/jasmine-ajax`。

这个时候我们需要去修改 `test/boot.ts` 文件，因为每次跑具体测试代码之前会先运行该文件，我们可以在这里去初始化 `jasmine-ajax`。

    const JasmineCore = require('jasmine-core')
    // @ts-ignore
    global.getJasmineRequireObj = function() {
      return JasmineCore
    }
    require('jasmine-ajax')

1
2
3
4
5
6

这里为了让 `jasmine-ajax` 插件运行成功，我们需要手动添加全局的 `getJasmineRequireObj` 方法，参考 issue
(opens new window)。

接下来，我们就开始编写请求模块的单元测试。

## # 测试代码编写

`test/requests.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')
        })
      })
    })

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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267

我们要注意的一些点，在这里列出：

- beforeEach & afterEach

beforeEach
(opens new window)表示每个测试用例运行前的钩子函数，在这里我们执行 `jasmine.Ajax.install()` 安装 `jasmine.Ajax`。

afterEach
(opens new window)表示每个测试用例运行后的钩子函数，在这里我们执行 `jasmine.Ajax.uninstall()` 卸载 `jasmine.Ajax`。

- `getAjaxRequest`

`getAjaxRequest` 是我们在 `test/helper.ts` 定义的一个辅助方法，通过 `jasmine.Ajax.requests.mostRecent()` 拿到最近一次请求的 `request` 对象，这个 `request` 对象是 `jasmine-ajax` 库伪造的 `xhr` 对象，它模拟了 `xhr` 对象上的方法，并且提供一些 `api` 让我们使用，比如 `request.respondWith` 方法返回一个响应。

- 异步测试

注意到我们这里大部分的测试用例不再是同步的代码了，几乎都是一些异步逻辑，Jest 非常好地支持异步测试代码
(opens new window)。通常有 2 种解决方案。

第一种是利用 `done` 参数，每个测试用例函数有一个 `done` 参数，一旦我们使用了该参数，只有当 `done` 函数执行的时候表示这个测试用例结束。

第二种是我们的测试函数返回一个 Promise 对象，一旦这个 Promise 对象 `resolve` 了，表示这个测试结束。

- expect.any(constructor)

它表示匹配任意由 `constructor` 创建的对象实例。

- `request.eventBus.trigger`

由于 `request.responseTimeout` 方法内部依赖了 `jasmine.clock` 方法会导致运行失败，这里我直接用了 `request.eventBus.trigger('timeout')` 方法触发了 `timeout` 事件。因为这个方法不在接口定义中，所以需要加 `// @ts-ignore`。

另外，我们在测试中发现 2 个 case 没有通过。

第一个是 `should treat method value as lowercase string`，这个测试用例是我们发送请求的 `method` 需要转换成小写字符串，这么做的目的也是为了之后 `flattenHeaders` 能正常处理这些 `method`，所以我们需要修改源码逻辑。

`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()

        // ...
      }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在合并配置后，我们需要把 `config.method` 转成小写字符串。

另一个是 `should return JSON when rejecting`，这个测试用例是当我们发送请求失败后，也能把响应数据转换成 JSON 格式，所以也需要修改源码逻辑。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

除了对正常情况的响应数据做转换，我们也需要对异常情况的响应数据做转换。

至此我们完成了 `ts-axios` 库对请求模块的测试，下一节课我们会从业务的角度来测试 `headers` 模块。

编辑
(opens new window)
#TypeScript
上次更新: 2022/12/14, 19:36:42
辅助模块单元测试
headers 模块单元测试

← 辅助模块单元测试 headers 模块单元测试→

最近更新

  01
Git修改分支名
08-11

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

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

更多文章>

Copyright © 2016-2022 Hesb | MIT License

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