间接输入

2024-09-21  本文已影响0人  sweetBoy_9126

依赖函数调用

import {userAge} from './user'

export function doubleUserAge(): number {
  return userAge() * 2
}
export function userAge() {
  return 18
}

export function fetchUserAge() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve(18)
    }, 0)
  })
}
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

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. 不想让当前的测试用例文件共享一个值
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
  })
})
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. 处理异步
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
}
export function userAge() {
  return 18
}

export function fetchUserAge(): Promise<number> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve(18)
    }, 0)
  })
}
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
  })
})

第三方库

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

对象

对象属性

import {User} from './UserClass'
export function doubleUserAge() :number {
  const user = new User()
  console.log(user)
  return user.age * 2
}
export class User {
  age: number = 18;
  name: string = 'lifa'
  getAge() {
    return this.age
  }
}
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')
  })
})

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')
  })
上一篇下一篇

猜你喜欢

热点阅读