Утвердить вызов функции в зависимости другого модуля с Jest - PullRequest
1 голос
/ 19 сентября 2019

У меня есть следующий код, который я пытаюсь охватить:

// @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)

1 Ответ

2 голосов
/ 19 сентября 2019

Мои два цента в том, что издевательство над зависимостью randomNumber не является правильным подходом для проверки этой функциональности.

Однако я собираюсь ответить на основной вопрос здесь и посмотреть, как мы можем пройти этот тест.Затем перейдем к моим дополнительным мыслям о лучшем способе проверки этого в следующем обновлении.

Утверждение против 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.mock вызовы поверх import 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:


Почему это не хороший тест?

Главным образом потому, что тест тесно связан с кодом.Если вы сравниваете тест и SUT, этот дублирующий код будет виден.

Лучше всего вообще не надругаться / шпионить (см. Classist vs. Mockist TDD школ) и проверять SUT с динамически генерируемым набором данных и весов, который, в свою очередь, утверждает, что это «достаточно хорошо».

Подробнее об этом я расскажу в обновлении.


Лучший тест

Проверка деталей реализации randomPick также не является хорошей идеей и по другой причине.Такой тест не может проверить правильность алгоритма, поскольку он только проверяет звонки, которые он делает.Если есть ошибка в крайнем случае, она недостаточно закрыта, чтобы ее можно было устранить.

Насмешка / шпионаж, как правило, выгоден, когда мы хотим отстаивать связь объектов;в случаях, когда сообщение фактически подтверждает правильность, например, «утверждают, что оно попадает в базу данных» ;но здесь это не так.

Идея лучшего тестового примера может состоять в том, чтобы энергично применять SUT и утверждать, что это "достаточно хорошо" для того, что он делает.

Предоставьте 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)
  })
})
...