如何编写测试用例来检查数组中随机创建数组时的数字是否唯一?

How to write a test case to check if numbers in an array are unique when the array is created randomly?

提问人:spacePirate 提问时间:7/28/2023 最后编辑:jonrsharpespacePirate 更新时间:8/14/2023 访问量:100

问:

我正在为一个彩票函数编写一个规范,该函数应该创建一个介于 1 和 49 之间的七个(随机)数字数组。数组中的数字必须是唯一的,这样的结果是无效的。[1, 2, 3, 4, 5, 6, 6]

如何确保测试不是偶然的绿色?

我的一个测试检查数组中的数字是否唯一。现在看来我有一个“片状”规格,因为它可以是红色或绿色(随机)。

我通过多次运行测试用例来“解决”了这个问题,但我想知道是否有更优雅的解决方案。我想要一种安全的方法来检查是否可靠地避免重复。lottery()

测试用例(重复测试):

it('TC05 - lottery() each array item must be a unique number', () => {

    // function to check if an array has duplicates
    const hasDuplicates = arr => {
        return (new Set(arr)).size !== arr.length;
    }

    // run check for duplicates x times
    const REPETITIONS = 32
    let seriesOfTests = []
    for (let i = 0; i < REPETITIONS; i++) {
        if (hasDuplicates(lottery())) {
            seriesOfTests.push(true)
        } else {
            seriesOfTests.push(false)
        }
    }

    // check series to ensure each single test passed
    let seriesSummery = seriesOfTests.includes(true)

    // test case
    expect(seriesSummery).must.be.false()
})

彩票函数(带有 if 语句以避免重复):

// lottery returns an array consists of 7 random numbers between 1 and 49
const lottery = () => {
    let result = []

    for (let i = 0; i < 7; i++) {
        const newNbr = randomNbr(49)

        if (result.find(el => el === newNbr) === undefined) {
            result.push(newNbr)
        } else {
            i -= 1
        }
    }
    return result
}
Javascript TDD

评论

0赞 pilchard 7/28/2023
只需填充一个数组并随机播放它 从数组中随机采样
0赞 jonrsharpe 7/28/2023
@pilchard这是一个实现,但关于测试的问题仍然存在。
0赞 pilchard 7/28/2023
@jonrsharpe很公平,但通过改进实现,可以简化测试。已知唯一数组的随机子集将始终是唯一的
0赞 jonrsharpe 7/28/2023
@pilchard不是真的,你测试的是行为而不是实现。好的测试可以让你自信地在当前实现和你的建议之间切换。
1赞 jonrsharpe 7/28/2023
或者只是用 Set 来做,但关键是一旦你有了测试,你就可以自由地重构。

答:

-1赞 axiac 7/28/2023 #1

您需要将生成随机数的函数替换为测试双精度值(也称为 mock)。配置“mock”(实际上是一个测试存根)以返回某个数字序列(它们不会是随机的,您可以决定返回哪些值),然后验证函数生成的随机数列表是否包含您期望它包含的数字,给定测试存根生成的“随机”数字。lottery()

一个简单的例子,使用 Jest

// I assume that the function `randomNbr()` is exported by file `randomNbr.js`
import { randomNbr } from 'randomNbr';

jest.mock('randomNbr');

it('generates a list of unique values', () => {
  randomNbr.mockReturnValueOnce(7);
  randomNbr.mockReturnValueOnce(2);
  randomNbr.mockReturnValueOnce(1);
  randomNbr.mockReturnValueOnce(7);
  randomNbr.mockReturnValueOnce(6);
  randomNbr.mockReturnValueOnce(3);
  randomNbr.mockReturnValueOnce(3);
  randomNbr.mockReturnValueOnce(7);
  randomNbr.mockReturnValueOnce(2);
  randomNbr.mockReturnValueOnce(5);
  randomNbr.mockReturnValueOnce(9);
  randomNbr.mockReturnValueOnce(2);
  randomNbr.mockReturnValueOnce(8);
  // ... put here at least the number of unique values that `lottery()` returns

  const numbers = lottery();

  expect(numbers).toEqual([7, 2, 1, 6, 3, 5, 9]);
});

假设输入是已知的(每次调用返回的值),并且算法需要生成 7 个随机数,并且没有重复,则预期的输出也是已知的。randomNbr()


测试为绿色后,可以重构函数以改进其代码。lottery()

现在它使用 Array.find() 而不是查找列表是否包含某个值。Array.includes() 速度更快,它更好地表达了代码意图。读者无需阅读文档即可理解使用的代码;他们必须阅读文档以了解返回的内容以及返回时间。Array.includes()Array.includes()Array.find()undefined

此外,循环和令人困惑,循环甚至不正确。如果生成重复数字,则生成包含 7 个以上值的列表。如果 ,由于某种原因卡住了,并且总是返回相同的值(就像在上面的测试中发生的那样),则永远不会完成。fori = -1forrandomNbr()lottery()randomNbr()lottery()

让我们尝试从需求开始编写代码。该函数必须:lottery()

  • 返回 7 个值;
  • 这些值必须是随机的;
  • 这些值必须不同。

第三个要求告诉我们,有时,它必须生成 7 个以上的随机值。限制为 7 次迭代的循环不正确。让我们生成随机的不同值,直到我们有 7 个。for

// using `const` and arrow functions for anything else than inline callbacks
// is a stupid fashion that has many drawbacks and no benefit
function lottery() {
  // start with an empty list
  let result = []

  do {
    // generate a random number
    const nb = randomNbr(49);

    // check for duplicates
    if (result.includes(nb)) {
      // generated a duplicate, ignore it, restart the loop
      continue;
    }

    // it's good, keep it in the list
    result.push(nb)

    // stop when the list reaches the desired size
  } while (result.length < 7);

  return result
}
0赞 VoiceOfUnreason 7/28/2023 #2

TDD+随机很难,而且已经很难了很久。

我发现唯一令人满意的一般方法是将问题重构为三个部分

  • 一个确定性函数,它接受一个位序列并计算一个值
  • 随机位的来源
  • 将两者结合在一起的功能

第一个功能是我们可以通过测试来驱动的东西。另外两个部分与更难约束的东西耦合,因此我们通过确保代码“如此简单,显然没有缺陷”来弥补。


对于您的特定问题:只有 (49 选择 7) 个数组满足您的约束。因此,一个按索引“查找”满足示例的约束的函数,结合范围内的随机值源(49 选择 7)可以满足您的需求。

考虑这个问题的一个方便方法是,将我们可能的数组集合想象成一个“压缩顺序”的序列,我们的(随机生成的)索引用于将该特定解决方案从序列中拉出。

现在,您可能不希望 (49 选择 7) 数组在内存中徘徊,因此您真正想要的是一个生成器函数,该函数在给定可接受范围内的索引时生成答案。

好消息是:一旦你研究了这个概念,生成器功能就不太难实现。我从中学到的解释在这里,这里是对同一问题的另一种看法。

0赞 Muhammad Farag 8/14/2023 #3

我唯一能想到的测试随机行为就是反复运行测试,直到你确信它不会失败。如果我想测试一枚公平的硬币,我必须把它翻转足够多的次数,才能看到它“几乎”是 50% 的正面和 50% 的反面。

同样,如果我想确保这个数组是唯一的,请在每次测试运行中多次运行检查。这应该会增加我们的信心,但不会达到100%。您可以运行检查 999,999,999,它仅在第 1,000,000,000 次失败,但这极不可能。

根据业务用例的不同,这种不太可能的机会可能非常昂贵。比如,阵列的唯一性可能每百万年失败一次,但如果失败,公司将无法恢复或可能面临法律后果等。在这种情况下,我建议对数组的唯一性进行断言。测试以确保在数组不唯一时触发此断言。如果在每次运行中运行 1000 次唯一性测试没有涵盖极端情况,这将为您提供生产安全网。

最后,我建议使用 Set 而不是 Array。它不仅可以保证独特性,还可以更好地传达意图。这里起作用的是集合的长度。

实现应该不太容易出错。我们将随机生成的值添加到集合中,当集合达到所需的大小时,我们就完成了。这是一个更具确定性的解决方案。