【发布时间】:2020-10-09 11:28:05
【问题描述】:
这是我第一次使用 Jest,我尝试测试一个具有许多这样的值的函数:
const convertConfig = require('../tools/convertNumWord/config');
const numToWordConstructor = require('../tools/convertNumWord/num2words/numToWords');
const wordToNumConstructor = require('../tools/convertNumWord/words2num/wordsToNum');
describe('EN-CONFIG', () => {
const config = convertConfig['en']; // It returns an object with configuration values for the language
const numToWord = numToWordConstructor(config); // It return a function builds with config object
const wordToNum = wordToNumConstructor(config);
for (let i = 0; i <= 4; i++) {
it(`Test for value ${i}`, () => {
expect(wordToNum(numToWord(i))).toBe(Number(i));
});
}
});
Jest 返回此错误:
● ENGLISH CONFIG › Test for value 2
expect(received).toBe(expected) // Object.is equality
Expected: 2
Received: 1
69 | for (let i = 0; i <= 4; i++) {
70 | it(`Test for value ${i}`, () => {
> 71 | expect(wordToNum(numToWord(i))).toBe(Number(i));
| ^
72 | });
73 | }
74 | });
at Object.<anonymous> (__tests__/loopConvertNum.test.js:71:39)
● ENGLISH CONFIG › Test for value 3
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: 1
69 | for (let i = 0; i <= 4; i++) {
70 | it(`Test for value ${i}`, () => {
> 71 | expect(wordToNum(numToWord(i))).toBe(Number(i));
| ^
72 | });
73 | }
74 | });
at Object.<anonymous> (__tests__/loopConvertNum.test.js:71:39)
● ENGLISH CONFIG › Test for value 4
expect(received).toBe(expected) // Object.is equality
Expected: 4
Received: 1
69 | for (let i = 0; i <= 4; i++) {
70 | it(`Test for value ${i}`, () => {
> 71 | expect(wordToNum(numToWord(i))).toBe(Number(i));
| ^
72 | });
73 | }
74 | });
at Object.<anonymous> (__tests__/loopConvertNum.test.js:71:39)
Test Suites: 1 failed, 2 passed, 3 total
Tests: 3 failed, 1 todo, 3 passed, 7 total
Snapshots: 0 total
Time: 1.773s
Ran all test suites.
我的第一个测试有效,但是 2 天后,Jest 似乎没有为每一轮 for 循环执行我的函数,好像它在内存中保留了值(通常是第二轮循环的值)和 toBe 测试最终是错误的。在某些情况下,我的函数的结果是完全不一致的(空值或从前一个结果的某种增量)。当我使用具有相同参数的节点启动我的函数时,它运行良好。
我尝试了 it.each,但问题仍然存在。每种模式对我都不好,因为我想测试我的函数的很多很多值????。
根据您的反馈,这里是导入的两个函数和配置常量。 但是这些函数可以很好地处理在我的循环中发送的参数。问题似乎不是来自返回函数的原因,因为当我使用 node 单独启动它们时,它们可以很好地使用与我的 Jest 测试的 for 循环中相同的参数。
这是numToWordConstructor中导入的函数:
const config = require('../config');
const _ = require('lodash');
const numToWordsConstructor = config => {
config = _.cloneDeep(config);
const letters = {
..._.invertBy(config.oneDigits),
..._.invertBy(config.doubleDigits),
..._.invertBy(config.tripleDigits),
};
const hundredWords = config.hundredWords;
const oneDigits = config.oneDigits;
const doubleDigits = config.doubleDigits;
const tripleDigits = config.isEachHundredsWords ? config.tripleDigits : null;
const separators = config.separators;
let words = [];
// Convertion function
const num2Words = number => {
let result = '';
const numbersWords = Object.keys(letters);
const nbString = number.toString();
const nb = parseFloat(nbString.replace(/ /gi, ''));
if (nb > 999999999999)
throw new Error(
'Function numToWords for number',
number,
': Number to big',
);
if (isNaN(nb))
throw new Error(
'Function numToWords for number',
number,
'Not a valid number',
);
if (Math.ceil(nb) != nb) {
separateFloatNumbers = nbString.split('.');
return (
num2Words(separateFloatNumbers[0]) +
' virgule ' +
num2Words(separateFloatNumbers[1])
);
}
if (Number(number) === 0) return oneDigits[0][0];
// Build numByThousand object to split number to 3 digits arrays
let numByThousand = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]];
if (numbersWords.includes(nbString)) {
result = letters[nbString][0];
} else {
const nbReversedDigits = nbString
.split('')
.map(digit => parseInt(digit))
.reverse();
numByThousand = numByThousand
.map(thousand => {
if (nbReversedDigits.length >= 3) {
return (thousand = nbReversedDigits.splice(0, 3).reverse());
} else {
return (thousand = [0, 0, 0]
.map(
defaultDigit =>
nbReversedDigits.splice(0, 1)[0] || defaultDigit,
)
.reverse());
}
})
.reverse();
}
// Build array of words depending of numByThousand
const convertNumByThousandToWords = numByThousand => {
const hundred = numByThousand[0] * 100;
const ten = numByThousand[1] * 10 + numByThousand[2];
const newWords = [];
if (numByThousand[0] > 0) {
if (config.isEachHundredsWords) {
newWords.push(tripleDigits[hundred][0]);
} else if (numByThousand[0] === 1 && !config.isOneBeforeOnlyOne[0]) {
newWords.push(config.hundredWords[0]);
} else {
newWords.push(oneDigits[numByThousand[0]][0]);
newWords.push(config.hundredWords[0]);
}
}
if (ten > 0) {
if (Object.keys(doubleDigits).includes(ten.toString())) {
newWords.push(doubleDigits[ten][0]);
return newWords;
} else {
if (numByThousand[1] > 0)
newWords.push(doubleDigits[numByThousand[1] * 10][0]);
if (numByThousand[2] > 0)
newWords.push(oneDigits[numByThousand[2]][0]);
return newWords;
}
}
return newWords;
};
if (config.noMoreMillionSeparator) separators.push(config.separators[0]);
numByThousand.map((thousand, i) => {
if (thousand.reduce((acc, cur) => acc + cur) > 0) {
words = _.concat(words, convertNumByThousandToWords(thousand));
// Handle thousand separators
separators[config.separators.length - i - 1] &&
words.push(separators[config.separators.length - i - 1][0]);
}
});
//Handle specifics spelling rules
words = config.spellingRules(words, oneDigits, doubleDigits);
// console.log('words : ', words.join(' '));
return words.join(' ');
};
return num2Words;
};
module.exports = numToWordsConstructor;
这是wordToNumConstructor中导入的函数:
const _ = require('lodash');
const arrayAwareInvert = obj => {
return _.reduce(
obj,
(result, values, key) => {
return _.reduce(
values,
(result, value) => {
result[value] = key;
return result;
},
result,
);
},
{},
);
};
const word2numConstructor = config => {
config = _.cloneDeep(config);
const separators = config.separators;
const hundredWords = config.hundredWords;
const oneDigits = arrayAwareInvert(config.oneDigits);
const doubleDigits = arrayAwareInvert(config.doubleDigits);
const tripleDigits = config.isEachHundredsWords
? arrayAwareInvert(config.tripleDigits)
: null;
const oneDigitsWords = Object.values(config.oneDigits).reduce(
(acc, cur) => acc.concat(cur),
[],
);
const doubleDigitsWords = Object.values(config.doubleDigits).reduce(
(acc, cur) => acc.concat(cur),
[],
);
const tripleDigitsWords = Object.values(config.tripleDigits).reduce(
(acc, cur) => acc.concat(cur),
[],
);
return stringNumber => {
separators.forEach(thousandSeparators =>
thousandSeparators.forEach(separator => {
// Remove spaces from separator's word in sentence to not split it with split(' ') later
const regex = new RegExp(`${separator}`, 'gi');
stringNumber = stringNumber.replace(
regex,
separator.split(' ').join(''),
);
// Remove spaces from separator's word
return separator.split(' ').join('');
}),
);
let thousandsWords = [];
if (config.noMoreMillionSeparator) {
thousandsWords = separators[0];
} else {
thousandsWords = _.flatten(separators);
}
stringNumber.trim();
// Apply on stringNumber the rules of locale key of config object
stringNumber = config.rulesOnSentence(stringNumber);
// Apply lowerCase on stringNumber and split words at each spaces
let words = stringNumber.toLowerCase().split(' ');
// Apply on each words the rules of locale key of config object
words = config.rulesOnWords(words);
const convertWordsByThousandToNumber = wordsByThousand => {
return wordsByThousand.map(values => {
if (values.length === 0) return 0;
let thousand = null;
let temp;
// Function to handle ten and unit if temp is hundred value
const handleTen_Unit = (i, coef = 100) => {
if (!values[i] || thousandsWords.includes(values[i])) {
thousand = temp * coef;
} else if (oneDigitsWords.includes(values[i])) {
thousand = temp * coef + Number(oneDigits[values[i]]);
} else if (doubleDigitsWords.includes(values[i])) {
if (!values[i + 1] || thousandsWords.includes(values[i + 1])) {
thousand = temp * coef + Number(doubleDigits[values[i]]);
} else if (oneDigitsWords.includes(values[i + 1])) {
thousand =
temp * coef +
Number(doubleDigits[values[i]]) +
Number(oneDigits[values[i + 1]]);
}
}
};
//Convert values
if (values.length === 1 && thousandsWords.includes(values[0])) {
thousand = 1;
} else if (hundredWords.includes(values[0])) {
temp = 1;
handleTen_Unit(1);
} else if (doubleDigitsWords.includes(values[0])) {
temp = Number(doubleDigits[values[0]]);
if (!values[1] || thousandsWords.includes(values[1])) {
thousand = temp;
} else if (oneDigitsWords.includes(values[1])) {
thousand = temp + Number(oneDigits[values[1]]);
}
} else if (oneDigitsWords.includes(values[0])) {
temp = Number(oneDigits[values[0]]);
if (!values[1] || thousandsWords.includes(values[1])) {
thousand = temp;
} else if (hundredWords.includes(values[1])) {
handleTen_Unit(2);
} else {
thousand = temp;
}
} else if (tripleDigitsWords.includes(values[0])) {
temp = Number(tripleDigits[values[0]]);
if (!values[1] || thousandsWords.includes(values[1])) {
thousand = temp;
} else {
handleTen_Unit(1, 1);
}
}
return thousand;
});
};
const buildWordsByThousand = words => {
const wordsByThousand = [];
separators
.slice(0) // Make a shallow copy
.reverse()
.map(thousandSeparators => {
const index = _.findIndex(words, word =>
thousandSeparators.includes(word),
);
index > -1
? wordsByThousand.push(words.splice(0, index + 1))
: wordsByThousand.push([]);
});
wordsByThousand.push(words); // Push the rest of words for hundred's part
return wordsByThousand;
};
let results = [];
let indexOfMillionWords = -1;
words.map((word, i) => {
if (separators[1].includes(word)) indexOfMillionWords = i;
});
if (config.noMoreMillionSeparator && indexOfMillionWords >= 0) {
const wordsAboveMillion = words.splice(indexOfMillionWords + 1);
const wordsOverMillion = words.splice(0, words.length - 1);
const wordsByThousandOverMillion = buildWordsByThousand(
wordsOverMillion,
).splice(1); // Splice is necessary to remove array of million thousand part
const wordsByThousandAboveMillion = buildWordsByThousand(
wordsAboveMillion,
).splice(1); // Splice is necessary to remove array of million thousand part
results = results.concat(
convertWordsByThousandToNumber(wordsByThousandOverMillion),
);
results = results.concat(
convertWordsByThousandToNumber(wordsByThousandAboveMillion),
);
} else {
const wordsByThousand = buildWordsByThousand(words);
results = results.concat(convertWordsByThousandToNumber(wordsByThousand));
}
results;
return results
.reverse()
.reduce(
(acc, cur, i) =>
acc === null || cur === null ? null : acc + cur * Math.pow(1000, i),
0,
);
};
};
module.exports = word2numConstructor;
这是 const config = convertConfig['en'] 的结果:
{
rulesOnSentence: sentence => {
// Write your rules's function to apply on sentence before splitted it
return sentence
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[\-]/g, ' ')
.replace(/[^a-zA-Z ]/g, '');
},
rulesOnWords: words => {
// Write your rules's function to apply on words array (after to have splitted sentence)
return words.filter(word => word !== 'and');
},
spellingRules: (words, oneDigits, doubleDigits) => {
const unit = _.flatten(Object.values(oneDigits).slice(1));
const ten = _.flatten(
[20, 30, 40, 50, 60, 70, 80, 90].map(num => doubleDigits[num]),
);
const newWords = [...words];
let j = 0;
words.forEach((word, i) => {
// Hyphenate all compound numbers
if (ten.includes(word) && unit.includes(words[i + 1])) {
newWords.splice(i - j, 2, word + '-' + words[i + 1]);
j++;
}
});
return newWords;
},
noMoreMillionSeparator: false,
separators: [
['thousand', 'thousands'],
['million', 'millions'],
['billion', 'billions'],
],
hundredWords: ['hundred', 'hundreds'],
isOneBeforeOnlyOne: [true, true, true, true],
isEachHundredsWords: false,
tripleDigits: {},
oneDigits: {
0: ['zero'],
1: ['one'],
2: ['two'],
3: ['three'],
4: ['four'],
5: ['five'],
6: ['six'],
7: ['seven'],
8: ['eight'],
9: ['nine'],
},
doubleDigits: {
10: ['ten'],
11: ['eleven'],
12: ['twelve'],
13: ['thirteen'],
14: ['fourteen'],
15: ['fifteen'],
16: ['sixteen'],
17: ['seventeen'],
18: ['eighteen'],
19: ['nineteen'],
20: ['twenty'],
30: ['thirty'],
40: ['forty'],
50: ['fifty'],
60: ['sixty'],
70: ['seventy'],
80: ['eighty'],
90: ['ninety'],
},
}
提前感谢您的宝贵反馈。
【问题讨论】:
-
问题出在您的代码中。不将 wordToNum 和 numToWord 测试分开是一种不好的测试实践,因为这不允许轻松确定其中哪些失败了。这些函数是有状态的,至少是因为
words数组。 Jest 似乎没有在 for 循环的每一轮中执行我的函数,就好像它在内存中保留了值一样 - Jest 没有也不能做类似的事情,但你的代码可以。您分配 wordToNum 和 numToWord 一次而不是在 beforeEach 中可能会导致测试交叉污染。如果使用 beforeEach,您可能不会看到此错误。 -
@EstusFlask 谢谢。我无法将每个函数的测试分开,因为如果我想测试 1000 个值,我必须手动编写 numToWord 的所有结果...... ????好的,谢谢您的“有状态”领导,因为我相信就是这样。配置变量正在由第一个循环修改。 “Jest 没有也不能做这样的事情,但你的代码可以”:谢谢你提供这些信息,但我没有看到 Jest 函数的源代码,我不知道是否没有缓存值.我想我会用你的答案找到解决方案!!!!非常感谢!
-
如果 Jest 会缓存函数调用,这将使它完全无法用于测试任何东西。通常在单元测试中,目标不是测试所有可能的组合,只是一些预期的组合和边缘情况。如果它适用于 42,您可以假设它也适用于 43。所以你可以测试例如0, 1, 2, 10, 100, 1.5, -1, ... 等以及无效值。这将提供更可靠的测试结果,而不是仅仅确认这两个功能相互抵消,而这并不能告诉您它们实际上工作得如何。
-
我并不是说您应该手动输入值。在这种情况下,我最终可能会得到包含数字和字符串对的夹具文件,然后分别针对它测试 wordToNum 和 numToWord。一个夹具很可能可以从它们的输出中生成并由人类验证一次。既然你知道有状态可以使它以不同的方式工作,它可能应该在测试中涵盖。拥有两个带有 beforeEach 和没有它的测试套件可能是一个安全的举措。
-
感谢您的回答。但是,我的函数必须适用于配置对象中任何语言的任何值。因此,如果 wordTonum(numToWord(i)) 适用于 42,我不能假设它适用于 43,因为可能存在 spellingRules 或字符串预处理规则(rulesOnSentence 或 rulesOnWords)。我必须根据我的算法测试数字部分的所有可能性,因为每种语言都可以对一个或多个数字或数字部分有自己的特殊性。
标签: javascript unit-testing testing jestjs