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')})})})
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: AxiosResponseaxios('/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'] = tokenreturn data}})return getAjaxRequest().then(request => {expect(request.requestHeaders['X-Authorization']).toEqual(token)})})})
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')})})})
注意在 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()})})})
注意,由于 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()})})})
静态方法模块单元测试
静态方法模块为 axios 对象添加了 2 个静态方法,我们需要为其编写单元测试。test/static.spec.ts:
import axios from '../src/index'describe('promise', () => {test('should support all', done => {let fulfilled = falseaxios.all([true, false]).then(arg => {fulfilled = arg[0]})setTimeout(() => {expect(fulfilled).toBeTruthy()done()}, 100)})test('should support spread', done => {let sum = 0let fulfilled = falselet result: anyaxios.all([123, 456]).then(axios.spread((a, b) => {sum = a + bfulfilled = truereturn 'hello world'})).then(res => {result = res})setTimeout(() => {expect(fulfilled).toBeTruthy()expect(sum).toBe(123 + 456)expect(result).toBe('hello world')done()}, 100)})})
补充未覆盖的代码测试
我们发现,跑完测试后,仍有一些代码没有覆盖到测试,其中 core/xhr.ts 文件的第 43 行:
if (responseType) {request.responseType = responseType}
我们并未在测试中设置过 responseType,因此我们在 test/requests.spect.ts 文件中补充相关测试:
test('should support array buffer response', done => {let response: AxiosResponsefunction 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-ignoreresponse: str2ab('Hello world')})setTimeout(() => {expect(response.data.byteLength).toBe(22)done()}, 100)})})
另外我们发现 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 库。
