如何正确约束“任意”UUID 生成?

How to properly constrain `arbitrary` UUID-Generation?

我正在尝试为我的某些类型创建 Arbitrary 个实例,以便在 QuickCheck 属性 测试中使用。我需要随机生成的 UUID,但不允许使用全零 (nil) UUID 的约束 - 即 00000000-0000-0000-0000-000000000000。因此,我设置了以下生成器:

nonzeroIdGen :: Gen UUID.UUID
nonzeroIdGen = arbitrary `suchThat` (not . UUID.null)

我在 Arbitrary 实例中使用如下:

instance Arbitrary E.EventId where
    arbitrary = do
        maybeEid <- E.mkEventId <$> nonzeroIdGen
        return $ fromJust maybeEid

一般来说,这是不安全的代码;但是为了测试,假设保证非零 UUID,我认为 fromJust 没问题。

mkEventId 定义为

mkEventId :: UUID.UUID -> Maybe EventId
mkEventId uid = EventId <$> validateId uid

使用 EventId 围绕 UUID.UUID

的新类型包装器
validateId :: UUID.UUID -> Maybe UUID.UUID
validateId uuid = if UUID.null uuid then Nothing else Just uuid

令我惊讶的是,由于上述代码生成的全零 UUID,我的测试失败了。 mkEventId 中的 trace 显示如下:

00000001-0000-0001-0000-000000000001
Just (EventId {getEventId = 00000001-0000-0001-0000-000000000001})

00000000-0000-0000-0000-000000000000
Nothing
    Create valid Events. FAILED [1]

第一个生成的 ID 很好,第二个是全零,尽管我的 nonzeroIdGen 生成器来自上面。我错过了什么?

我通常发现在这种情况下,使用 newtype 定义 Arbitrary 的实例组合更好。这是我为有效 UUID 值制作的:

newtype NonNilUUID = NonNilUUID { getNonNilUUID :: UUID } deriving (Eq, Show)

instance Arbitrary NonNilUUID where
  arbitrary = NonNilUUID <$> arbitrary `suchThat` (/= nil)

然后您可以从这个实例组合其他 Arbitrary 个实例,就像我在这里使用 Reservation 数据类型所做的那样:

newtype ValidReservation =
  ValidReservation { getValidReservation :: Reservation } deriving (Eq, Show)

instance Arbitrary ValidReservation where
  arbitrary = do
    (NonNilUUID rid) <- arbitrary
    (FutureTime d) <- arbitrary
    n <- arbitrary
    e <- arbitrary
    (QuantityWithinCapacity q) <- arbitrary
    return $ ValidReservation $ Reservation rid d n e q

注意模式匹配 (NonNilUUID rid) <- arbitraryrid 解构为 UUID 值。

您可能会注意到我还为 Reservation 数据类型创建了 ValidReservation newtype。我 consistently do this to avoid orphan instances,并避免使用 QuickCheck 依赖项污染我的领域模型。 (我不反对 QuickCheck,但特定于测试的功能不属于 'production' 代码。)

这里显示的所有代码都是available in context on GitHub