美文网首页
间接输入

间接输入

作者: sweetBoy_9126 | 来源:发表于2024-09-21 16:54 被阅读0次

    依赖函数调用

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

    相关文章

      网友评论

          本文标题:间接输入

          本文链接:https://www.haomeiwen.com/subject/kkozljtx.html