defaults 模块单元测试

defaults 模块为请求配置提供了一些默认的属性和方法,我们需要为其编写单元测试。
test/defaults.spec.ts

  1. import axios, { AxiosTransformer } from '../src/index'
  2. import { getAjaxRequest } from './helper'
  3. import { deepMerge } from '../src/helpers/util'
  4. describe('defaults', () => {
  5. beforeEach(() => {
  6. jasmine.Ajax.install()
  7. })
  8. afterEach(() => {
  9. jasmine.Ajax.uninstall()
  10. })
  11. test('should transform request json', () => {
  12. expect((axios.defaults.transformRequest as AxiosTransformer[])[0]({ foo: 'bar' })).toBe('{"foo":"bar"}')
  13. })
  14. test('should do nothing to request string', () => {
  15. expect((axios.defaults.transformRequest as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
  16. })
  17. test('should transform response json', () => {
  18. const data = (axios.defaults.transformResponse as AxiosTransformer[])[0]('{"foo":"bar"}')
  19. expect(typeof data).toBe('object')
  20. expect(data.foo).toBe('bar')
  21. })
  22. test('should do nothing to response string', () => {
  23. expect((axios.defaults.transformResponse as AxiosTransformer[])[0]('foo=bar')).toBe('foo=bar')
  24. })
  25. test('should use global defaults config', () => {
  26. axios('/foo')
  27. return getAjaxRequest().then(request => {
  28. expect(request.url).toBe('/foo')
  29. })
  30. })
  31. test('should use modified defaults config', () => {
  32. axios.defaults.baseURL = 'http://example.com/'
  33. axios('/foo')
  34. return getAjaxRequest().then(request => {
  35. expect(request.url).toBe('http://example.com/foo')
  36. delete axios.defaults.baseURL
  37. })
  38. })
  39. test('should use request config', () => {
  40. axios('/foo', {
  41. baseURL: 'http://www.example.com'
  42. })
  43. return getAjaxRequest().then(request => {
  44. expect(request.url).toBe('http://www.example.com/foo')
  45. })
  46. })
  47. test('should use default config for custom instance', () => {
  48. const instance = axios.create({
  49. xsrfCookieName: 'CUSTOM-XSRF-TOKEN',
  50. xsrfHeaderName: 'X-CUSTOM-XSRF-TOKEN'
  51. })
  52. document.cookie = instance.defaults.xsrfCookieName + '=foobarbaz'
  53. instance.get('/foo')
  54. return getAjaxRequest().then(request => {
  55. expect(request.requestHeaders[instance.defaults.xsrfHeaderName!]).toBe('foobarbaz')
  56. document.cookie =
  57. instance.defaults.xsrfCookieName +
  58. '=;expires=' +
  59. new Date(Date.now() - 86400000).toUTCString()
  60. })
  61. })
  62. test('should use GET headers', () => {
  63. axios.defaults.headers.get['X-CUSTOM-HEADER'] = 'foo'
  64. axios.get('/foo')
  65. return getAjaxRequest().then(request => {
  66. expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
  67. delete axios.defaults.headers.get['X-CUSTOM-HEADER']
  68. })
  69. })
  70. test('should use POST headers', () => {
  71. axios.defaults.headers.post['X-CUSTOM-HEADER'] = 'foo'
  72. axios.post('/foo', {})
  73. return getAjaxRequest().then(request => {
  74. expect(request.requestHeaders['X-CUSTOM-HEADER']).toBe('foo')
  75. delete axios.defaults.headers.post['X-CUSTOM-HEADER']
  76. })
  77. })
  78. test('should use header config', () => {
  79. const instance = axios.create({
  80. headers: {
  81. common: {
  82. 'X-COMMON-HEADER': 'commonHeaderValue'
  83. },
  84. get: {
  85. 'X-GET-HEADER': 'getHeaderValue'
  86. },
  87. post: {
  88. 'X-POST-HEADER': 'postHeaderValue'
  89. }
  90. }
  91. })
  92. instance.get('/foo', {
  93. headers: {
  94. 'X-FOO-HEADER': 'fooHeaderValue',
  95. 'X-BAR-HEADER': 'barHeaderValue'
  96. }
  97. })
  98. return getAjaxRequest().then(request => {
  99. expect(request.requestHeaders).toEqual(
  100. deepMerge(axios.defaults.headers.common, axios.defaults.headers.get, {
  101. 'X-COMMON-HEADER': 'commonHeaderValue',
  102. 'X-GET-HEADER': 'getHeaderValue',
  103. 'X-FOO-HEADER': 'fooHeaderValue',
  104. 'X-BAR-HEADER': 'barHeaderValue'
  105. })
  106. )
  107. })
  108. })
  109. test('should be used by custom instance if set before instance created', () => {
  110. axios.defaults.baseURL = 'http://example.org/'
  111. const instance = axios.create()
  112. instance.get('/foo')
  113. return getAjaxRequest().then(request => {
  114. expect(request.url).toBe('http://example.org/foo')
  115. delete axios.defaults.baseURL
  116. })
  117. })
  118. test('should not be used by custom instance if set after instance created', () => {
  119. const instance = axios.create()
  120. axios.defaults.baseURL = 'http://example.org/'
  121. instance.get('/foo')
  122. return getAjaxRequest().then(request => {
  123. expect(request.url).toBe('/foo')
  124. })
  125. })
  126. })

transform 模块单元测试

transform 模块用来定义请求和响应的转换方法,我们需要为其编写单元测试。

  1. import axios, { AxiosResponse, AxiosTransformer } from '../src/index'
  2. import { getAjaxRequest } from './helper'
  3. describe('transform', () => {
  4. beforeEach(() => {
  5. jasmine.Ajax.install()
  6. })
  7. afterEach(() => {
  8. jasmine.Ajax.uninstall()
  9. })
  10. test('should transform JSON to string', () => {
  11. const data = {
  12. foo: 'bar'
  13. }
  14. axios.post('/foo', data)
  15. return getAjaxRequest().then(request => {
  16. expect(request.params).toBe('{"foo":"bar"}')
  17. })
  18. })
  19. test('should transform string to JSON', done => {
  20. let response: AxiosResponse
  21. axios('/foo').then(res => {
  22. response = res
  23. })
  24. getAjaxRequest().then(request => {
  25. request.respondWith({
  26. status: 200,
  27. responseText: '{"foo": "bar"}'
  28. })
  29. setTimeout(() => {
  30. expect(typeof response.data).toBe('object')
  31. expect(response.data.foo).toBe('bar')
  32. done()
  33. }, 100)
  34. })
  35. })
  36. test('should override default transform', () => {
  37. const data = {
  38. foo: 'bar'
  39. }
  40. axios.post('/foo', data, {
  41. transformRequest(data) {
  42. return data
  43. }
  44. })
  45. return getAjaxRequest().then(request => {
  46. expect(request.params).toEqual({ foo: 'bar' })
  47. })
  48. })
  49. test('should allow an Array of transformers', () => {
  50. const data = {
  51. foo: 'bar'
  52. }
  53. axios.post('/foo', data, {
  54. transformRequest: (axios.defaults.transformRequest as AxiosTransformer[]).concat(function(
  55. data
  56. ) {
  57. return data.replace('bar', 'baz')
  58. })
  59. })
  60. return getAjaxRequest().then(request => {
  61. expect(request.params).toBe('{"foo":"baz"}')
  62. })
  63. })
  64. test('should allowing mutating headers', () => {
  65. const token = Math.floor(Math.random() * Math.pow(2, 64)).toString(36)
  66. axios('/foo', {
  67. transformRequest: (data, headers) => {
  68. headers['X-Authorization'] = token
  69. return data
  70. }
  71. })
  72. return getAjaxRequest().then(request => {
  73. expect(request.requestHeaders['X-Authorization']).toEqual(token)
  74. })
  75. })
  76. })

xsrf 模块单元测试

xsrf 模块提供了一套防御 xsrf 攻击的解决方案,我们需要为其编写单元测试。
test/xsrf.spec.ts

  1. import axios from '../src/index'
  2. import { getAjaxRequest } from './helper'
  3. describe('xsrf', () => {
  4. beforeEach(() => {
  5. jasmine.Ajax.install()
  6. })
  7. afterEach(() => {
  8. jasmine.Ajax.uninstall()
  9. document.cookie =
  10. axios.defaults.xsrfCookieName + '=;expires=' + new Date(Date.now() - 86400000).toUTCString()
  11. })
  12. test('should not set xsrf header if cookie is null', () => {
  13. axios('/foo')
  14. return getAjaxRequest().then(request => {
  15. expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
  16. })
  17. })
  18. test('should set xsrf header if cookie is set', () => {
  19. document.cookie = axios.defaults.xsrfCookieName + '=12345'
  20. axios('/foo')
  21. return getAjaxRequest().then(request => {
  22. expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
  23. })
  24. })
  25. test('should not set xsrf header for cross origin', () => {
  26. document.cookie = axios.defaults.xsrfCookieName + '=12345'
  27. axios('http://example.com/')
  28. return getAjaxRequest().then(request => {
  29. expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBeUndefined()
  30. })
  31. })
  32. test('should set xsrf header for cross origin when using withCredentials', () => {
  33. document.cookie = axios.defaults.xsrfCookieName + '=12345'
  34. axios('http://example.com/', {
  35. withCredentials: true
  36. })
  37. return getAjaxRequest().then(request => {
  38. expect(request.requestHeaders[axios.defaults.xsrfHeaderName!]).toBe('12345')
  39. })
  40. })
  41. })

注意在 afterEach 函数中我们清空了 xsrf 相关的 cookie。

上传下载模块单元测试

上传下载模块允许我们监听上传和下载的进度,我们需要为其编写单元测试。
test/progress.spec.ts

  1. import axios from '../src/index'
  2. import { getAjaxRequest } from './helper'
  3. describe('progress', () => {
  4. beforeEach(() => {
  5. jasmine.Ajax.install()
  6. })
  7. afterEach(() => {
  8. jasmine.Ajax.uninstall()
  9. })
  10. test('should add a download progress handler', () => {
  11. const progressSpy = jest.fn()
  12. axios('/foo', { onDownloadProgress: progressSpy })
  13. return getAjaxRequest().then(request => {
  14. request.respondWith({
  15. status: 200,
  16. responseText: '{"foo": "bar"}'
  17. })
  18. expect(progressSpy).toHaveBeenCalled()
  19. })
  20. })
  21. test('should add a upload progress handler', () => {
  22. const progressSpy = jest.fn()
  23. axios('/foo', { onUploadProgress: progressSpy })
  24. return getAjaxRequest().then(request => {
  25. // Jasmine AJAX doesn't trigger upload events.Waiting for jest-ajax fix
  26. // expect(progressSpy).toHaveBeenCalled()
  27. })
  28. })
  29. })

注意,由于 jasmine-ajax 插件不会派发 upload 事件,这个未来可以通过我们自己编写的 jest-ajax 插件来解决,目前不写断言的情况它会直接通过。

HTTP 授权模块单元测试

HTTP 授权模块为我们在请求头中添加 Authorization 字段,我们需要为其编写单元测试。
test/auth.spec.ts

  1. import axios from '../src/index'
  2. import { getAjaxRequest } from './helper'
  3. describe('auth', () => {
  4. beforeEach(() => {
  5. jasmine.Ajax.install()
  6. })
  7. afterEach(() => {
  8. jasmine.Ajax.uninstall()
  9. })
  10. test('should accept HTTP Basic auth with username/password', () => {
  11. axios('/foo', {
  12. auth: {
  13. username: 'Aladdin',
  14. password: 'open sesame'
  15. }
  16. })
  17. return getAjaxRequest().then(request => {
  18. expect(request.requestHeaders['Authorization']).toBe('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
  19. })
  20. })
  21. test('should fail to encode HTTP Basic auth credentials with non-Latin1 characters', () => {
  22. return axios('/foo', {
  23. auth: {
  24. username: 'Aladßç£☃din',
  25. password: 'open sesame'
  26. }
  27. })
  28. .then(() => {
  29. throw new Error(
  30. 'Should not succeed to make a HTTP Basic auth request with non-latin1 chars in credentials.'
  31. )
  32. })
  33. .catch(error => {
  34. expect(/character/i.test(error.message)).toBeTruthy()
  35. })
  36. })
  37. })

静态方法模块单元测试

静态方法模块为 axios 对象添加了 2 个静态方法,我们需要为其编写单元测试。
test/static.spec.ts

  1. import axios from '../src/index'
  2. describe('promise', () => {
  3. test('should support all', done => {
  4. let fulfilled = false
  5. axios.all([true, false]).then(arg => {
  6. fulfilled = arg[0]
  7. })
  8. setTimeout(() => {
  9. expect(fulfilled).toBeTruthy()
  10. done()
  11. }, 100)
  12. })
  13. test('should support spread', done => {
  14. let sum = 0
  15. let fulfilled = false
  16. let result: any
  17. axios
  18. .all([123, 456])
  19. .then(
  20. axios.spread((a, b) => {
  21. sum = a + b
  22. fulfilled = true
  23. return 'hello world'
  24. })
  25. )
  26. .then(res => {
  27. result = res
  28. })
  29. setTimeout(() => {
  30. expect(fulfilled).toBeTruthy()
  31. expect(sum).toBe(123 + 456)
  32. expect(result).toBe('hello world')
  33. done()
  34. }, 100)
  35. })
  36. })

补充未覆盖的代码测试

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

  1. if (responseType) {
  2. request.responseType = responseType
  3. }

我们并未在测试中设置过 responseType,因此我们在 test/requests.spect.ts 文件中补充相关测试:

  1. test('should support array buffer response', done => {
  2. let response: AxiosResponse
  3. function str2ab(str: string) {
  4. const buff = new ArrayBuffer(str.length * 2)
  5. const view = new Uint16Array(buff)
  6. for (let i = 0; i < str.length; i++) {
  7. view[i] = str.charCodeAt(i)
  8. }
  9. return buff
  10. }
  11. axios('/foo', {
  12. responseType: 'arraybuffer'
  13. }).then(data => {
  14. response = data
  15. })
  16. getAjaxRequest().then(request => {
  17. request.respondWith({
  18. status: 200,
  19. // @ts-ignore
  20. response: str2ab('Hello world')
  21. })
  22. setTimeout(() => {
  23. expect(response.data.byteLength).toBe(22)
  24. done()
  25. }, 100)
  26. })
  27. })

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

  1. method = 'get'

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

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

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

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