Мои два цента в том, что издевательство над зависимостью 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)
})
})