间接输入

依赖函数调用

  • index.ts
import {userAge} from './user'

export function doubleUserAge(): number {
  return userAge() * 2
}
  • user.ts
export function userAge() {
  return 18
}

export function fetchUserAge() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve(18)
    }, 0)
  })
}
  • case.spec.ts
import { doubleUserAge } from ".";
import {it, expect, describe} from 'vitest'

describe('间接input', () => {
  it('first', () => {
    const r = doubleUserAge()
    expect(r).toBe(36)
  })
})

问题:我们 userAge 里的值可能变,如果一旦修改了,那么我们这个单元测试就不通过了
解决方式:我们是否可以控制间接 Input 的值(userAge)

使用 vitest vi

  • case.sepc.ts
import { doubleUserAge } from ".";
import {vi, it, expect, describe} from 'vitest'

vi.mock('./user.ts', () => {
  return {
    userAge: () => 2
  }
})
describe('间接input', () => {
  it('first', () => {
    const r = doubleUserAge()
    expect(r).toBe(4)
  })
})
  1. 只要在外层使用 vi 需修改了 user
    age 里的值,在这个测试 case 里全局都会改变
it('second', () => {
    // 2
    console.log(userAge())
  })
  1. 使用了 vi.mock 在编译的时候会提到最顶部
    console.log(userAge()) // 2
vi.mock('./user.ts', () => {
  return {
    userAge: () => 2
  }
})
  1. 不想让当前的测试用例文件共享一个值
  • 使用 mocked
vi.mock('./user.ts')
describe('间接input', () => {
  it('first', () => {
    vi.mocked(userAge).mockReturnValue(2)
    const r = doubleUserAge()
    expect(r).toBe(4)
  })
  it('second', () => {
    vi.mocked(userAge).mockReturnValue(4)
    console.log(userAge()) // 4
  })
})
  • 使用 doMock
describe('间接input', () => {
  it('first', async () => {
    vi.doMock('./user', () => {
      return {
        userAge: () => 2
      }
    })
    const {doubleUserAge} = await import('./index')
    const r = doubleUserAge()
    expect(r).toBe(4)
  })
  it('second', () => {
    console.log(userAge()) // oldValue
  })
})

优化

describe('间接input', () => {
  beforeEach(() => {

    vi.doMock('./user', () => {
      return {
        userAge: () => 2
      }
    })
  })
  it('first', async () => {
    const {doubleUserAge} = await import('./index')
    const r = doubleUserAge()
    expect(r).toBe(4)
  })
  it('second', () => {
    console.log(userAge()) // oldValue
  })
})
  1. 处理异步
  • index.ts
import {fetchUserAge, userAge} from './user'

export function doubleUserAge(): number {
  return userAge() * 2
}
export async function fetchDoubleUserAge() :Promise<number> {
  const userAge = await fetchUserAge()
  return userAge * 2
}
  • user.ts
export function userAge() {
  return 18
}

export function fetchUserAge(): Promise<number> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve(18)
    }, 0)
  })
}
  • case.spec.ts
import { doubleUserAge, fetchDoubleUserAge } from ".";
import {vi, it, expect, describe, beforeEach} from 'vitest'
import { userAge } from "./user";
vi.mock('./user', () => {
  return {
    fetchUserAge: () => Promise.resolve(2)
  }
})
describe('间接input', () => {
  beforeEach(() => {

    vi.doMock('./user', () => {
      return {
        userAge: () => 2
      }
    })
  })
  it('first', async () => {
    const r = await fetchDoubleUserAge()
    expect(r).toBe(4)
  })
  it('second', () => {
    // console.log(userAge()) // oldValue
  })
})

第三方库

  • third-party-modules.ts
import axios from 'axios'
interface User {
  name: string;
  age: number
}
export async function doubleUserAge() {
  const user: User = await axios('/user/1')
  return user.age * 2
}
  • spec.ts
import {test, vi, expect} from 'vitest'
import { doubleUserAge } from './third-party-modules'
import axios from 'axios'
vi.mock('axios')

test('第三模块的处理 axios', async () => {
  vi.mocked(axios).mockResolvedValue({name: 'lifa', age: 18})
  const r = await doubleUserAge()
  expect(r).toBe(36)
})
  • get 或 post
export async function doubleUserAge() {
  const user: User = await axios.get('/user/1')
  return user.age * 2
}

test('第三模块的处理 axios', async () => {
  vi.mocked(axios.get).mockResolvedValue({name: 'lifa', age: 18})
  const r = await doubleUserAge()
  expect(r).toBe(36)
})

对象

对象属性

  • use-class.ts
import {User} from './UserClass'
export function doubleUserAge() :number {
  const user = new User()
  console.log(user)
  return user.age * 2
}
  • useClass.ts
export class User {
  age: number = 18;
  name: string = 'lifa'
  getAge() {
    return this.age
  }
}
  • spec.ts
import {it, expect, describe, vi} from 'vitest'
import { doubleUserAge } from './use-class'
vi.mock('./UserClass', () => {
  return {
    User: class User {
      age: number = 2
    }
  }
})
describe('使用 class 形式', () => {
  it('属性', () => {
    const r = doubleUserAge()
    expect(r).toBe(4)
  })
})

方法

describe('使用 class 形式', () => {
 // it('属性', () => {
  //   const r = doubleUserAge()
  //   expect(r).toBe(4)
  // })
  it('方法', () => {
    User.prototype.getAge = () => {
      return 2
    }

    const r = doubleUserAge()
    expect(r).toBe(4)
  })
})

变量

// user-variable.ts
import {name, gold} from './config'
export function tellName() {
  console.log(gold)
  return name + '-heiheihei'
}

// user-variable.spec.ts
import {it, expect, describe, vi} from 'vitest'
import { tellName } from './user-variable'
vi.mock('./config', () => {
  return {
    name: 'c'
  }
})
describe('使用变量的形式', () => {
  it('tell me', () => {
    const r = tellName()
    expect(r).toBe('c-heiheihei')
  })
})

问题这样虽然我们的测试能通过,但是因为我们 mock 改写了返回值,我们 mock 里只返回了 name,所以我们在 tellName 里打印 gold 就会报错,因为mock里没返回 gold,它把我们之前的值覆盖了

如果我们只想改变其中某个值,还想保留之前的其他的属性和方法,那么直接通过 mock 的工厂函数里的参数获取就可以了

vi.mock('./config', async (importOriginal) => {
  const config = await importOriginal()
  return {
    ...config as any,
    name: 'c'
  }
})

// 也可以
vi.mock('./config', async (importOriginal) => {
  const config = await vi.importActual('./config')
  return {
    ...config as any,
    name: 'c'
  }
})

环境变量

  1. process
// env.ts
export function doubleUserAge() {
  return Number(process.env.USER_AGE) * 2
}

// env.spec.ts
import {it, expect, vi, afterEach} from 'vitest'
import { doubleUserAge } from './env'
afterEach(() => {
  // 每个case结束后都重置环境变量
  vi.unstubAllEnvs()
})

it('process', () => {
  // process.env.USER_AGE = '2'
  vi.stubEnv('USER_AGE', '2')
  const r = doubleUserAge()
  expect(r).toBe(4)
})
  1. import.meta
    同样也是用 vi.stubEnv 和 unstubAllEnvs 这两个方法
it('import.meta', () => {
  vi.stubEnv('VITE_USER_AGE', '2')
  const r = doubleUserAge()
  expect(r).toBe(4)
})

全局 global

// global.ts
export function doubleUserAge() {
  return lifa.age * 2
}
export function doubleHeight() {
  return innerHeight * 2
}

// spec.ts
import {vi, it, expect, describe} from 'vitest'
import { doubleHeight, doubleUserAge } from './global'

describe('global', () => {
  it('double user age', () => {
    vi.stubGlobal('lifa', {age: 18})

    const r = doubleUserAge()

    expect(r).toBe(36)
  })
  it('double inner height', () => {
    vi.stubGlobal('innerHeight', 100)
    const r = doubleHeight()
    expect(r).toBe(200)
  })
})

间接层的处理技巧

// window.ts
export function innerHeightFn() {
  return innerHeight
}

// global.ts
import { innerHeightFn } from "./window"
export function doubleHeight() {
  return innerHeightFn() * 2
}

// spec.ts
vi.mock('./window.ts', () => {
  return {
    innerHeightFn: () => 200,
  }
})
it('function', () => {
    const r = doubleHeight()
    expect(r).toBe(400)
  })

依赖注入

将依赖的模块通过参数传入,替换掉直接的依赖

1. 函数实现

比如:

import {readFileSync} from 'fs'
export function readAndProcessFile(filePath: string): string {
  const content: string = readFileSync(filePath, {encoding: 'utf-8'})
  return content + ''-> test unit"
} 

使用依赖注入的方法将依赖的 readFileSync 通过参数传入

export function readAndProcessFile(filePath: string, fileReader): string {
  const content: string = fileReader.read(filePath)
  return content + ''-> test unit"
} 

调用

import {readAndProcessFile} from './readAndProcessFile'
import {readFileSync} from 'fs'
class FileReader {
  read(filePath: string) {
    readFileSync(filePath, {encoding: 'utf-8'})
  }
}
const result = readAndProcessFile('example.txt', new FileReader())

对应的测试用例

import {it, expect, describe} from 'vitest'
import {readAndProcessFile} from './readAndProcessFile'

describe('di function', () => {
  it('read and process file', () => {
    class StubFileReader {
      read(filePath: string) {
        return 'lifa'
      }
    }
    const result = readAndProcessFile('./test', new StubFileReader())
    expect(result).toBe('lifa-> test unit')
  })
})
  • 依赖倒置原则
    高层模块不应该依赖低层模块,两者都应该依赖其抽象
    抽象不应该依赖细节,细节应该依赖于抽象



    上面的a就是我们的 readAndProcessFile.ts 文件,fs 模块就是 b,a又叫做高层模块,b是低层模块,倒置后,a依赖于接口,b是实现接口

class 实现

改造前:

import { readFileSync } from 'fs'
export class ReadAndProcessFile {
  run(filePath: string) {
    const content = readFileSync(filePath, {encoding: 'utf-8'})
    return content + '->unit test'
  }
}

方式1:构造函数
把依赖通过构造器传过来

interface FileReader {
  read(filePath: string): string
}
export class ReadAndProcessFile {
  private _fileReader: FileReader
  constructor(fileReader: FileReader) {
    this._fileReader = fileReader
  }
  run(filePath: string) {
    const content = this._fileReader.read(filePath)
    return content + '->unit test'
  }
}

对应测试用例

import {it, expect, describe} from 'vitest'
import {ReadAndProcessFile, FileReader} from './ReadAndProcessFile'

describe('di class', () => {
  it('构造函数', () => {
    class StubFileReader implements FileReader {
      read(filePath: string): string {
        return 'lifa'
      }
    }
    const readAndProcessFile = new ReadAndProcessFile(new StubFileReader())
    expect(readAndProcessFile.run('./test')).toBe('lifa->unit test')
  })
})

方式2:属性

export class ReadAndProcessFile {
  private _fileReader: FileReader
  run(filePath: string) {
    const content = this._fileReader.read(filePath)
    return content + '->unit test'
  }
  set fileReader(fileReader: FileReader) {
    this._fileReader = fileReader
  }
}

对应用例:

  it('属性', () => {

    class StubFileReader implements FileReader {
      read(filePath: string): string {
        return 'lifa'
      }
    }
    const readAndProcessFile = new ReadAndProcessFile()
    readAndProcessFile.fileReader = new StubFileReader()
    expect(readAndProcessFile.run('./test')).toBe('lifa->unit test')
  })

方式3:方法

export class ReadAndProcessFile {
  private _fileReader: FileReader
  run(filePath: string) {
    const content = this._fileReader.read(filePath)
    return content + '->unit test'
  }
  setFileReader(fileReader: FileReader) {
    this._fileReader = fileReader
  }
}

测试用例

  it('方法', () => {

    class StubFileReader implements FileReader {
      read(filePath: string): string {
        return 'lifa'
      }
    }
    const readAndProcessFile = new ReadAndProcessFile()
    readAndProcessFile.setFileReader(new StubFileReader())
    expect(readAndProcessFile.run('./test')).toBe('lifa->unit test')
  })
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容