javascript - 使用 Jest 在另一个模块的依赖项中断言函数调用

标签 javascript unit-testing mocking jestjs

我尝试涵盖以下代码:

// @flow strict

import { bind, randomNumber } from 'Utils'
import { AbstractOperator } from './AbstractOperator'

export class Randomize extends AbstractOperator {
  // ...

  randomPick (dataset: Array<string>, weights: ?Array<number>): number {
    if (!weights) { return randomNumber(0, (dataset.length - 1)) }

    const sumOfWeights: number = weights.reduce((a, b) => a + b)
    let randomWeight = randomNumber(1, sumOfWeights)
    let position: number = -1

    for (let i = 0; i < dataset.length; i++) {
      randomWeight = randomWeight - weights[i]
      if (randomWeight <= 0) {
        position = i
        break
      }
    }

    return position
  }
}

这是测试覆盖率:
import { Randomize } from './Randomize'

const dataset = [
  'nok',
  'nok',
  'nok',
  'ok',
  'nok'
]

const weights = [
  0,
  0,
  0,
  1,
  0
]

const randomNumber = jest.fn()

describe('operator Randomize#randomPick', () => {
  test('without weights, it calls `randomNumber`', () => {
    const randomizeOperator = new Randomize({}, [dataset], {})
    randomizeOperator.randomPick(dataset)

    expect(randomNumber).toBeCalledWith(0, dataset.length - 1)
  })
})

我正在努力确保 randomNumber被调用,但我得到的只是:
  ● operator Randomize#randomPick › without weights, it calls `randomNumber`

    expect(jest.fn()).toBeCalledWith(...expected)

    Expected: 0, 4

    Number of calls: 0

      33 |     randomizeOperator.randomPick(dataset)
      34 |
    > 35 |     expect(randomNumber).toBeCalledWith(0, dataset.length - 1)
         |                          ^
      36 |   })
      37 | })
      38 |

      at Object.toBeCalledWith (node_modules/jest-chain/dist/chain.js:15:11)
      at Object.toBeCalledWith (src/app/Services/Providers/Result/Resolvers/Operators/Randomize.test.js:35:26)

最佳答案

我的两分钱是 mock randomNumber依赖不是测试此功能的正确方法。

但是,我将在这里回答主要问题,看看我们如何才能通过该测试。然后将讨论我对在 future 更新中测试它的更好方法的其他想法。

反对 randomNumber称呼

拦截导入和模拟

代码的实际问题是 randomNumber模拟功能悬而未决。正如错误所暗示的,它没有被调用。

缺少的部分是拦截模块导入并使其外部调用 Utils.randomNumber触发模拟功能;这样我们就可以断言反对它。以下是拦截Utils的方法导入并模拟它:

// Signature is: 
// jest.mock(pathToModule: string, mockModuleFactory: Function)
jest.mock('Utils', () => ({
  randomNumber: jest.fn()
}))

现在每次调用Utils.randomNumber在测试期间将触发模拟功能,它不再悬而未决。

如果你想知道它是如何在幕后工作的,请查看 babel-plugin-jest-hoist吊机jest.mockimport 上调用s 正在编译为 CommonJS Jest-hijacked require来电。

根据情况,模拟整个模块可能会有问题。如果测试依赖于 Utils 的其他导出怎么办?模块?例如bind ?

有一些方法可以部分地模拟一个模块,一个函数,一个或两个类。但是,为了让您的测试通过,还有一种更简单的方法。

窥探它

解决方案是简单地监视 randomNumber称呼。这是一个完整的例子:

import { Randomize } from './Randomize'
import * as Utils from 'Utils'

// Sidenote: This values should probably be moved to a beforeEach()
// hook. The module-level assignment does not happen before each test.
const weights = [0, 0, 0, 1, 0]
const dataset = ['nok', 'nok', 'nok', 'ok', 'nok']

describe('operator Randomize#randomPick', () => {
  test('without weights, it calls `randomNumber`', () => {
    const randomizeOperator = new Randomize({}, [dataset], {})
    const randomNumberSpy = jest.spyOn(Utils, 'randomNumber')

    randomizeOperator.randomPick(dataset)

    expect(randomNumberSpy).toBeCalledWith(0, dataset.length - 1)
  })
})

希望这是一个通过测试,但非常脆弱。

总结一下,这些是在 Jest 背景下关于该主题的非常好的读物:
  • Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports
  • Understanding Jest mocks


  • 为什么这不是一个好的测试?

    主要是因为测试与代码紧密耦合。如果您比较测试和 SUT,可以看到重复代码。

    更好的方法是根本不模拟/监视任何东西(查看 Classist vs. Mockist TDD 学校)并使用一组动态生成的数据和权重来锻炼 SUT,这反过来又断言它“足够好”。

    我将在更新中详细说明这一点。

    更好的测试

    测试 randomPick 的实现细节由于另一个原因,这也不是一个好主意。这样的测试无法验证算法的正确性,因为它只是验证它所做的调用。如果存在边缘情况错误,则它的覆盖范围不足以击中它。

    当我们想要反对对象的通信时,Mocking/Spying 通常是有益的;在通信实际上是正确的断言的情况下,例如“断言它命中数据库”;但这里不是。

    一个更好的测试用例的想法可能是“大力”运行 SUT 并断言它对于它正在做的事情“足够好”;选择一个随机元素。这个想法得到了 Law of Large Numbers 的支持。 :

    "In probability theory, the law of large numbers (LLN) is a theorem that describes the result of performing the same experiment a large number of times. According to the law, the average of the results obtained from a large number of trials should be close to the expected value, and will tend to become closer to the expected value as more trials are performed." — Wikipedia



    为 SUT 提供相对较大的、动态生成的随机输入集,并断言它每次都通过:

    import { Randomize } from './Randomize'
    
    const exercise = (() => {
      // Dynamically generate a relatively large random set of input & expectations:
      // [ datasetArray, probabilityWeightsArray, expectedPositionsArray ]
      //
      // A sample manual set:  
      return [
        [['nok', 'nok', 'nok', 'ok', 'nok'], [0, 0, 0, 1, 0], [3]],
        [['ok', 'ok', 'nok', 'ok', 'nok'], [50, 50, 0, 0, 0], [0, 1]],
        [['nok', 'nok', 'nok', 'ok', 'ok'], [0, 0, 10, 60, 30], [2, 3, 4]]
      ]
    })()
    
    describe('whatever', () => {
      test.each(exercise)('look into positional each() params for unique names', (dataset, weights, expected) => {
        const randomizeOperator = new Randomize({}, [dataset, weights], {})
    
        const position = randomizeOperator.randomPick(dataset, weights)
    
        expect(position).toBeOneOf(expected)
      })
    })
    

    这是基于相同想法的另一种观点,不一定需要生成动态数据:

    import { Randomize } from './Randomize'
    
    const exercise = (() => {
      return [
        [
          ['moreok'], // expect "moreok" to have been picked more during the exercise.
          ['lessok', 'moreok'], // the dataset.
          [0.1, 99.90] // weights, preferring the second element over the first.
        ],
        [['moreok'], ['moreok', 'lessok'], [99, 1]],
        [['moreok'], ['lessok', 'moreok'], [1, 99]],
        [['e'], ['a', 'b', 'c', 'd', 'e'], [0, 10, 10, 0, 80]],
        [['d'], ['a', 'b', 'c', 'd'], [5, 20, 0, 75]],
        [['d'], ['a', 'd', 'c', 'b'], [5, 75, 0, 20]],
        [['b'], ['a', 'b', 'c', 'd'], [0, 80, 0, 20]],
        [['a', 'b'], ['a', 'b', 'c', 'd'], [50, 50]],
        [['b'], ['a', 'b', 'c'], [10, 60, 30]],
        [['b'], ['a', 'b', 'c'], [0.1, 0.6, 0.3]] // This one pinpoints a bug.
      ]
    })()
    
    const mostPicked = results => {
      return Object.keys(results).reduce((a, b) => results[a] > results[b] ? a : b )
    }
    
    describe('randompick', () => {
      test.each(exercise)('picks the most probable: %p from %p with weights: %p', (mostProbables, dataset, weights) => {
        const operator = new Randomize({}, [dataset, weights], {})
        const results = dataset.reduce((carry, el) => Object.assign(carry, { [el]: 0 }), {})
        // e.g. { lessok: 0, moreok: 0 }
    
        for (let i = 0; i <= 2000; i++) {
          // count how many times a dataset element has win the lottery!
          results[dataset[operator.randomPick(dataset, weights)]]++
        }
    
        // console.debug(results, mostPicked(results))
    
        expect(mostPicked(results)).toBeOneOf(mostProbables)
      })
    })
    

    更具可读性的测试

    当测试像上面一样被“功能噪音”污染时,它们变得难以阅读;它们不再用作文档。

    在这种情况下,开发自定义匹配器或测试替身有助于提高可读性。

    test.each([
      // ...
    ])('picks the most probable: %p from %p with weights: %p', mostProbables, dataset, weights) => {
        const results = []
        const operator = new Randomize(...whatever)
    
        ;[...Array(420).keys()].forEach(() => results.push(
          operator.randomPick(...whatever)
        )
    
        expect(results).toHaveMostFrequentElements(mostProbables)
    }
    

    这个习惯toHaveMostFrequentElements断言匹配器有助于消除测试中的“噪音”。

    关于javascript - 使用 Jest 在另一个模块的依赖项中断言函数调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58009041/

    相关文章:

    javascript - AngularJS 强制提交不发送一些隐藏的输入值

    unit-testing - 使用 templateUrl 时 Angular2 测试错误

    c# - 使用内存中的 sql lite 单元测试流利的 nhibernate 存储库 - 没有这样的表错误

    java - 在基于 TDD 的单元测试中处理 I/O

    unit-testing - 有什么工具可以记录单元测试的Java对象吗?

    grails - 模拟日志对象

    javascript - 有没有办法使用 sinon 测试下面的代码

    javascript - 如何定义 jquery/javascript 状态?

    javascript - Java Nashorn 实例方法

    unit-testing - 在 Dart 中重构友好的模拟