Skip to main content

[TS] ts-axios(23) - 单元测试(mergeConfig模块与请求取消模块)


mergeConfig模块单元测试

合并配置是 ts-axios 核心流程中非常重要的一个环节,我们需要为它的各种情况去编写测试。

测试代码编写

test/mergeConfig.spec.ts

import axios from '../src/index'
import mergeConfig from '../src/core/mergeConfig'

describe('mergeConfig', () => {
const defaults = axios.defaults

test('should accept undefined for second argument', () => {
expect(mergeConfig(defaults, undefined)).toEqual(defaults)
})

test('should accept an object for second argument', () => {
expect(mergeConfig(defaults, {})).toEqual(defaults)
})

test('should not leave references', () => {
const merged = mergeConfig(defaults, {})
expect(merged).not.toBe(defaults)
expect(merged.headers).not.toBe(defaults.headers)
})

test('should allow setting request options', () => {
const config = {
url: '__sample url__',
params: '__sample params__',
data: { foo: true }
}
const merged = mergeConfig(defaults, config)
expect(merged.url).toBe(config.url)
expect(merged.params).toBe(config.params)
expect(merged.data).toEqual(config.data)
})

test('should not inherit request options', () => {
const localDefaults = {
url: '__sample url__',
params: '__sample params__',
data: { foo: true }
}
const merged = mergeConfig(localDefaults, {})
expect(merged.url).toBeUndefined()
expect(merged.params).toBeUndefined()
expect(merged.data).toBeUndefined()
})

test('should return default headers if pass config2 with undefined', () => {
expect(
mergeConfig(
{
headers: 'x-mock-header'
},
undefined
)
).toEqual({
headers: 'x-mock-header'
})
})

test('should merge auth, headers with defaults', () => {
expect(
mergeConfig(
{
auth: undefined
},
{
auth: {
username: 'foo',
password: 'test'
}
}
)
).toEqual({
auth: {
username: 'foo',
password: 'test'
}
})
expect(
mergeConfig(
{
auth: {
username: 'foo',
password: 'test'
}
},
{
auth: {
username: 'baz',
password: 'foobar'
}
}
)
).toEqual({
auth: {
username: 'baz',
password: 'foobar'
}
})
})

test('should overwrite auth, headers with a non-object value', () => {
expect(
mergeConfig(
{
headers: {
common: {
Accept: 'application/json, text/plain, */*'
}
}
},
{
headers: null
}
)
).toEqual({
headers: null
})
})

test('should allow setting other options', () => {
const merged = mergeConfig(defaults, {
timeout: 123
})
expect(merged.timeout).toBe(123)
})
})

运行测试后我们发现 mergeConfig.ts 文件的分支覆盖率并未达到 100%,提示是 23 行,打开文件后发现最后一个 else 逻辑并未走到,也就是 val1undefined 的情况。但实际上即使 val1undefined,我们也是返回 undefined,也就是返回 val1,所以这块代码的逻辑可以优化。

function deepMergeStrat(val1: any, val2: any): any {
if (isPlainObject(val2)) {
return deepMerge(val1, val2)
} else if (typeof val2 !== 'undefined') {
return val2
} else if (isPlainObject(val1)) {
return deepMerge(val1)
} else {
return val1
}
}

2 个分支可以合并到一个分支,这样我们再次跑测试,分支覆盖率就可以达到 100% 了。

至此我们完成了 ts-axios 库对 mergeConfig 模块的测试,下一节课我们来测试取消模块相关代码。


请求取消模块单元测试

请求取消模块是 ts-axios 库核心流程其中一个分支,也是非常重要的模块,我们将从基础库和业务流程模块 2 个方面去编写单元测试。

Cancel 类单元测试

cancel/Cancel.spec.ts

import Cancel, { isCancel } from '../../src/cancel/Cancel'

describe('cancel:Cancel', () => {
test('should returns correct result when message is specified', () => {
const cancel = new Cancel('Operation has been canceled.')
expect(cancel.message).toBe('Operation has been canceled.')
})

test('should returns true if value is a Cancel', () => {
expect(isCancel(new Cancel())).toBeTruthy()
})

test('should returns false if value is not a Cancel', () => {
expect(isCancel({ foo: 'bar' })).toBeFalsy()
})
})

CancelToken 类单元测试

cancel/CancelToken.spec.ts

import CancelToken from '../../src/cancel/CancelToken'
import Cancel from '../../src/cancel/Cancel'
import { Canceler } from '../../src/types'

describe('CancelToken', () => {
describe('reason', () => {
test('should returns a Cancel if cancellation has been requested', () => {
let cancel: Canceler
let token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
expect(token.reason).toEqual(expect.any(Cancel))
expect(token.reason!.message).toBe('Operation has been canceled.')
})

test('should has no side effect if call cancellation for multi times', () => {
let cancel: Canceler
let token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
cancel!('Operation has been canceled.')
expect(token.reason).toEqual(expect.any(Cancel))
expect(token.reason!.message).toBe('Operation has been canceled.')
})

test('should returns undefined if cancellation has not been requested', () => {
const token = new CancelToken(() => {
// do nothing
})
expect(token.reason).toBeUndefined()
})
})

describe('promise', () => {
test('should returns a Promise that resolves when cancellation is requested', done => {
let cancel: Canceler
const token = new CancelToken(c => {
cancel = c
})
token.promise.then(value => {
expect(value).toEqual(expect.any(Cancel))
expect(value.message).toBe('Operation has been canceled.')
done()
})
cancel!('Operation has been canceled.')
})
})

describe('throwIfRequested', () => {
test('should throws if cancellation has been requested', () => {
let cancel: Canceler
const token = new CancelToken(c => {
cancel = c
})
cancel!('Operation has been canceled.')
try {
token.throwIfRequested()
fail('Expected throwIfRequested to throw.')
} catch (thrown) {
if (!(thrown instanceof Cancel)) {
fail('Expected throwIfRequested to throw a Cancel, but test threw ' + thrown + '.')
}
expect(thrown.message).toBe('Operation has been canceled.')
}
})

test('should does not throw if cancellation has not been requested', () => {
const token = new CancelToken(() => {
// do nothing
})
token.throwIfRequested()
})
})

describe('source', () => {
test('should returns an object containing token and cancel function', () => {
const source = CancelToken.source()
expect(source.token).toEqual(expect.any(CancelToken))
expect(source.cancel).toEqual(expect.any(Function))
expect(source.token.reason).toBeUndefined()
source.cancel('Operation has been canceled.')
expect(source.token.reason).toEqual(expect.any(Cancel))
expect(source.token.reason!.message).toBe('Operation has been canceled.')
})
})
})

注意,这里我们使用了 fail 函数表示一个测试的失败,这个并未在 Jest 文档中体现,但它是一个可以用的 API。

Cancel 业务逻辑单元测试

cancel.spec.ts

import axios from '../src/index'
import { getAjaxRequest } from './helper'

describe('cancel', () => {
const CancelToken = axios.CancelToken
const Cancel = axios.Cancel

beforeEach(() => {
jasmine.Ajax.install()
})

afterEach(() => {
jasmine.Ajax.uninstall()
})

describe('when called before sending request', () => {
test('should rejects Promise with a Cancel object', () => {
const source = CancelToken.source()
source.cancel('Operation has been canceled.')

return axios
.get('/foo', {
cancelToken: source.token
})
.catch(reason => {
expect(reason).toEqual(expect.any(Cancel))
expect(reason.message).toBe('Operation has been canceled.')
})
})
})

describe('when called after request has been sent', () => {
test('should rejects Promise with a Cancel object', done => {
const source = CancelToken.source()
axios
.get('/foo/bar', {
cancelToken: source.token
})
.catch(reason => {
expect(reason).toEqual(expect.any(Cancel))
expect(reason.message).toBe('Operation has been canceled.')
done()
})

getAjaxRequest().then(request => {
source.cancel('Operation has been canceled.')
setTimeout(() => {
request.respondWith({
status: 200,
responseText: 'OK'
})
}, 100)
})
})

test('calls abort on request object', done => {
const source = CancelToken.source()
let request: any
axios
.get('/foo/bar', {
cancelToken: source.token
})
.catch(() => {
expect(request.statusText).toBe('abort')
done()
})

getAjaxRequest().then(req => {
source.cancel()
request = req
})
})
})

describe('when called after response has been received', () => {
test('should not cause unhandled rejection', done => {
const source = CancelToken.source()
axios
.get('/foo', {
cancelToken: source.token
})
.then(() => {
window.addEventListener('unhandledrejection', () => {
done.fail('Unhandled rejection.')
})
source.cancel()
setTimeout(done, 100)
})

getAjaxRequest().then(request => {
request.respondWith({
status: 200,
responseText: 'OK'
})
})
})
})
})

注意这里我们使用了 done.fail 表示了一个异常的结束,这个并未在 Jest 文档中体现,但它是一个可以用的 API。

至此,我们完成了取消模块相关业务逻辑的单元测试,我们测试覆盖率达到了阈值,测试已经通过了。但是扔未达到我们的目标,还有很多 feature 是没有覆盖到的。接下来我们就完成剩余 feature 的编写单元测试。