目录

🌖 辅助模块单元测试

# 准备工作

通常会优先为一个库的辅助方法编写测试。

在这辅助方法为 src/helpers 目录下的模块。在 test 目录下创建一个 helpers 目录下,创建一个 boot.ts 空文件。给 Jest 配置了 setupFilesAfterEnv 指向了这个文件,之后会编写这个文件。

然后可以在控制台运行命令 npm test ,实际上执行了 jest --coverage 进行单元测试。此时运行会报错,因为没有匹配的测试文件( test 目录下没有 .spec.ts 结尾的文件)。

# 各个模块测试代码编写

# util 模块测试

创建文件 test/helpers/util.spec.ts

点击查看
import {
  deepMerge,
  extend,
  isDate,
  isFormData,
  isPlainObject,
  isURLSearchParams
} from '../../src/helpers/util'

describe('helpers:util', () => {
  describe('isXX', () => {
    test('should validate Date', () => {
      expect(isDate(new Date())).toBeTruthy()
      expect(isDate(Date.now())).toBeFalsy()
    })

    test('should validate PlainObject', () => {
      expect(isPlainObject({})).toBeTruthy()
      expect(isPlainObject(new Date())).toBeFalsy()
    })

    test('should validate FormData', () => {
      expect(isFormData(new FormData())).toBeTruthy()
      expect(isFormData({})).toBeFalsy()
    })

    test('should validate URLSearchParams', () => {
      expect(isURLSearchParams(new URLSearchParams())).toBeTruthy()
      expect(isURLSearchParams('foo=2&bar=2')).toBeFalsy()
    })
  })
})

describe('extend', () => {
  test('should be mutable', () => {
    const a = Object.create(null)
    const b = { foo: 123 }

    extend(a, b)
    expect(a.foo).toBe(123)
  })

  test('should extend properties', function() {
    const a = {foo: 123, bar: 456}
    const b = { bar: 789}
    const c= extend(a, b)

    expect(c.foo).toBe(123)
    expect(c.bar).toBe(789)
  })
})

describe('deepMerge', () => {
  test('should be immutable', () => {
    const a = Object.create(null)
    const b: any = { foo: 123 }
    const c: any = { bar: 456 }

    deepMerge(a, b, c)

    expect(typeof a.foo).toBe('undefined')
    expect(typeof a.bar).toBe('undefined')
    expect(typeof b.bar).toBe('undefined')
    expect(typeof c.foo).toBe('undefined')
  })

  test('should deepMerge properties', () => {
    const a = { foo: 123 }
    const b = { bar: 456 }
    const c = { foo: 789 }
    const d = deepMerge(a, b, c)

    expect(d.foo).toBe(789)
    expect(d.bar).toBe(456)
  })

  test('should deepMerge recursively', function() {
    const a = { foo: { bar: 123 } }
    const b = { foo: { baz: 456 }, bar: { qux: 789 } }
    const c = deepMerge(a, b)

    expect(c).toEqual({
      foo: {
        bar: 123,
        baz: 456
      },
      bar: {
        qux: 789
      }
    })
  })

  test('should remove all references from nested objects', () => {
    const a = { foo: { bar: 123 } }
    const b = {}
    const c = deepMerge(a, b)

    expect(c).toEqual({
      foo: {
        bar: 123
      }
    })

    expect(c.foo).not.toBe(a.foo)
  })

  test('should handle null and undefined arguments', () => {
    expect(deepMerge(undefined, undefined)).toEqual({})
    expect(deepMerge(undefined, { foo: 123 })).toEqual({ foo: 123 })
    expect(deepMerge({ foo: 123 }, undefined)).toEqual({ foo: 123 })

    expect(deepMerge(null, null)).toEqual({})
    expect(deepMerge(null, { foo: 123 })).toEqual({ foo: 123 })
    expect(deepMerge({ foo: 123 }, null)).toEqual({ foo: 123 })
  })
})
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

其中使用 describe 方法定义一组测试,支持嵌套, test 函数用来定义单个测试用例,是测试的最小单元; expect 是断言函数,判断代码的实际结果与预期结果是否一致,如果不一致就抛出错误。

测试文件编写好后可以单个测试用例运行;也可以运行命令 npm test 运行测试。

创建文件: test/helpers/cookie.spec.ts

点击查看
import cookie from '../../src/helpers/cookie'

describe('helpers:cookie', () => {
  test('should read cookies', () => {
    document.cookie = 'foo=bar'
    expect(cookie.read('foo')).toBe('bar')
  })

  test('should return null if cookie name is not exist', () => {
    document.cookie = 'foo=bar'
    expect(cookie.read('bar')).toBeNull()
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13

一般 jest 测试环境配置了 jsdom 能操作 DOM。如果不行需要加上 --env=jsdom 的配置(命令配置中)。

# data 模块测试

创建文件 test/helpers/data.spec.ts

点击查看
import { transformRequest, transformResponse } from '../../src/helpers/data'

describe('helpers:data', () => {
  describe('transformRequest', () => {
    test('should transform request data to string if data is a PlainObject', () => {
      const a = { a: 1 }
      expect(transformRequest(a)).toBe('{"a":1}')
    })

    test('should do nothing if data is not a PlainObject', () => {
      const a = new URLSearchParams('a=b')
      expect(transformRequest(a)).toBe(a)
    })
  })

  describe('transformResponse', () => {
    test('should transform response data to Object if data is a JSON string', () => {
      const a = '{"a": 2}'
      expect(transformResponse(a)).toEqual({ a: 2 })
    })

    test('should do nothing if data is a string but not a JSON string', () => {
      const a = '{a: 2}'
      expect(transformResponse(a)).toEqual('{a: 2}')
    })

    test('should do nothing if data is not a string', () => {
      const a = {a: 2}
      expect(transformResponse(a)).toBe(a)
    })

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

# error 模块测试

创建文件 test/helpers/test.spec.ts

点击查看
import { AxiosRequestConfig, AxiosResponse } from '../../src'
import { createError } from '../../src/helpers/error'

describe('helpers:error', () => {
  test('should create an Error with message, config, code, request, response and isAxiosError', () => {
    const request = new XMLHttpRequest()
    const config: AxiosRequestConfig = { method: 'post' }
    const response: AxiosResponse = {
      status: 200,
      statusText: 'OK',
      headers: null,
      request,
      config,
      data: { foo: 'bar' }
    }
    const error = createError('Boom!', config, 'SOMETHING', request, response)
    expect(error instanceof Error).toBeTruthy()
    expect(error.message).toBe('Boom!')
    expect(error.config).toBe(config)
    expect(error.code).toBe('SOMETHING')
    expect(error.request).toBe(request)
    expect(error.response).toBe(response)
    expect(error.isAxiosError).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

# headers 模块测试

创建文件 test/helpers/headers.spec.ts

点击查看
import { flattenHeaders, parseHeaders, processHeaders } from '../../src/helpers/headers'

describe('helpers:header', () => {
  describe('parseHeaders', () => {
    test('should parse headers', () => {
      const parsed = parseHeaders(
        'Content-Type: application/json\r\n' +
        'Connection: keep-alive\r\n' +
        'Transfer-Encoding: chunked\r\n' +
        'Date: Tue, 21 May 2019 09:23:44 GMT\r\n' +
        ':aa\r\n' +
        'key:'
      )

      expect(parsed['content-type']).toBe('application/json')
      expect(parsed['connection']).toBe('keep-alive')
      expect(parsed['transfer-encoding']).toBe('chunked')
      expect(parsed['date']).toBe('Tue, 21 May 2019 09:23:44 GMT')
      expect(parsed['key']).toBe('')
    })

    test('should return empty object if headers is empty string', () => {
      expect(parseHeaders('')).toEqual({})
    })
  })

  describe('processHeades', () => {
    test('should normalize Content-Type header name', () => {
      const headers: any = {
        'conTenT-Type': 'foo/bar',
        'Content-length': 1024
      }

      processHeaders(headers, {})
      expect(headers['Content-Type']).toBe('foo/bar')
      expect(headers['conTenT-Type']).toBeUndefined()
      expect(headers['Content-length']).toBe(1024)
    })

    test('should set Content-Type if not set and data is PlainObject', () => {
      const headers: any = {}
      processHeaders(headers, { a: 1 })
      expect(headers['Content-Type']).toBe('application/json;charset=utf-8')
    })

    test('should set not Content-Type if not set and data is not PlainObject', () => {
      const headers: any = {}
      processHeaders(headers, new URLSearchParams('a=b'))
      expect(headers['Content-Type']).toBeUndefined()
    })

    test('should do nothing if headers is undefined or null', () => {
      expect(processHeaders(undefined, {})).toBeUndefined()
      expect(processHeaders(null, {})).toBeNull()
    })
  })

  describe('flattenHeaders', () => {
    test('should flatten the headers and include common headers', () => {
      const headers = {
        Accept: 'application/json',
        common: {
          'X-COMMON-HEADER': 'commonHeaderValue'
        },
        get: {
          'X-GET-HEADER': 'getHeaderValue'
        },
        post: {
          'X-POST-HEADER': 'postHeaderValue'
        }
      }

      expect(flattenHeaders(headers, 'get')).toEqual({
        Accept: 'application/json',
        'X-COMMON-HEADER': 'commonHeaderValue',
        'X-GET-HEADER': 'getHeaderValue'
      })
    })

    test('should flatten the headers without common headers', () => {
      const headers = {
        Accept: 'application/json',
        get: {
          'X-GET-HEADER': 'getHeaderValue'
        }
      }

      expect(flattenHeaders(headers, 'patch')).toEqual({
        Accept: 'application/json'
      })
    })

    test('should do nothing if headers is undefined or null', () => {
      expect(flattenHeaders(undefined, 'get')).toBeUndefined()
      expect(flattenHeaders(null, 'post')).toBeNull()
    })

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

此处测试发现错误。 测试组的 should parse headers 测试没通过, expect(parsed['date']).toBe('Tue, 21 May 2019 09:23:44 GMT') 我们期望解析后的 date 字段是 Tue, 21 May 2019 09:23:44 GMT ,而实际的值是 Tue, 21 May 2019 09

说明 parseHeaders 的代码逻辑有漏洞,只考虑了第一个 : 符号,没有考虑后半部分也可能有 : 符号,因为现有逻辑会把字符串中 : 后面部分都截断。

修改后的 parasedHeaders

export function parseHeaders(headers: string): any {
  let parsed = Object.create(null)
  if (!headers) return parsed

  headers.split('\r\n').forEach(line => {
    let [key, ...vals] = line.split(':')
    key = key.trim().toLowerCase()
    if (!key) return
    let val = vals.join(':').trim()
    parsed[key] = val
  })

  return parsed
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# url 模块测试

创建文件 test/helpers/url.spec.ts :

点击查看
import { buildURL, combineURL, isAbsoluteURL, isURLSameOrigin } from '../../src/helpers/url'

describe('helpers:url', () => {
  describe('buildURL', () => {
    test('should support null params', () => {
      expect(buildURL('/foo')).toBe('/foo')
    })

    test('should support params', () => {
      expect(
        buildURL('/foo', {
          foo: 'bar'
        })
      ).toBe('/foo?foo=bar')
    })

    test('should ignore if some param value is null', () => {
      expect(
        buildURL('/foo', {
          foo: 'bar',
          baz: null
        })
      ).toBe('/foo?foo=bar')
    })

    test('should ignore if the only param value is null', () => {
      expect(
        buildURL('/foo', {
          foo: null
        })
      ).toBe('/foo')
    })

    test('should support object params', () => {
      expect(
        buildURL('/foo', {
          foo: {
            bar: 'baz'
          }
        })
      ).toBe('/foo?foo=' + encodeURI('{"bar":"baz"}'))
    })

    test('should support date params', () => {
      const date = new Date()

      expect(
        buildURL('/foo', {
          date
        })
      ).toBe('/foo?date=' + date.toISOString())
    })

    test('should support array params', () => {
      expect(
        buildURL('/foo', {
          foo: ['bar', 'baz']
        })
      ).toBe('/foo?foo[]=bar&foo[]=baz')
    })

    test('should support special char params', () => {
      expect(
        buildURL('/foo', {
          foo: '@:$'
        })
      ).toBe('/foo?foo=@:$')
    })

    test('should support existing params', () => {
      expect(
        buildURL('/foo?foo=bar', {
          bar: 'baz'
        })
      ).toBe('/foo?foo=bar&bar=baz')
    })

    test('should correct discard url hash mark', () => {
      expect(
        buildURL('/foo?foo=bar#hash', {
          query: 'baz'
        })
      ).toBe('/foo?foo=bar&query=baz')
    })

    test('should use serializer if provided', () => {
      const serializer = jest.fn(() => {
        return 'bar=baz'
      })
      const params = { foo: 'bar' }
      expect(buildURL('/foo?foo=bar#hash', params, serializer)).toBe('/foo?foo=bar&bar=baz')
      expect(serializer).toHaveBeenCalled()
      expect(serializer).toHaveBeenCalledWith(params)
    })

    test('should support URLSearchParams', () => {
      expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toBe('/foo?bar=baz')
    })
  })

  describe('isAbsoluteURL', () => {
    test('should return true if URL begins with valid scheme name', () => {
      expect(isAbsoluteURL('https://api.github.com/users')).toBeTruthy()
      expect(isAbsoluteURL('custom-scheme-v1.0://example.com/')).toBeTruthy()
      expect(isAbsoluteURL('HTTP://example.com/')).toBeTruthy()
    })

    test('should return false if URL begins with invalid scheme name', () => {
      expect(isAbsoluteURL('123://api.github.com/users')).toBeFalsy()
      expect(isAbsoluteURL('!valid://api.github.com/users')).toBeFalsy()
    })

    test('should return true if URL is protocol-relative', () => {
      expect(isAbsoluteURL('//example.com/')).toBeTruthy()
    })

    test('should return false if URL is relative', () => {
      expect(isAbsoluteURL('/foo')).toBeFalsy()
      expect(isAbsoluteURL('foo')).toBeFalsy()
    })
  })

  describe('combineURL', () => {
    test('should combine URL', () => {
      expect(combineURL('https://api.github.com', '/users')).toBe('https://api.github.com/users')
    })

    test('should remove duplicate slashes', () => {
      expect(combineURL('https://api.github.com/', '/users')).toBe('https://api.github.com/users')
    })

    test('should insert missing URL', () => {
      expect(combineURL('https://api.github.com', 'users')).toBe('https://api.github.com/users')
    })

    test('should not insert slash when relative url missing/empty', () => {
      expect(combineURL('https://api.github.com/users', '')).toBe('https://api.github.com/users')
    })

    test('should allow a single slash for relative url', () => {
      expect(combineURL('https://api.github.com/users', '/')).toBe('https://api.github.com/users/')
    })
  })

  describe('isURLSameOrigin', () => {
    test('should detect same origin', () => {
      expect(isURLSameOrigin(window.location.href)).toBeTruthy()
    })

    test('should detect same origin', () => {
      expect(isURLSameOrigin('https://api.github.com/users')).toBeFalsy()
    })

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

使用了 jest.fn (opens new window)去模拟了一个函数,这个也是在编写 Jest 测试中非常常用的一个 API。

📢 上次更新: 2022/09/02, 10:18:16