依赖函数调用
- 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)
})
})
- 只要在外层使用 vi 需修改了 user
age 里的值,在这个测试 case 里全局都会改变
it('second', () => {
// 2
console.log(userAge())
})
- 使用了 vi.mock 在编译的时候会提到最顶部
console.log(userAge()) // 2
vi.mock('./user.ts', () => {
return {
userAge: () => 2
}
})
- 不想让当前的测试用例文件共享一个值
- 使用 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
})
})
- 处理异步
- 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'
}
})
环境变量
- 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)
})
- 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')
})