QuickCheck 顺序映射键生成

QuickCheck sequential Map key generation

我正在尝试测试自定义数据类型的逻辑。它接收一个 Map Int String 作为参数,然后我需要将一个元素添加到对象内的 Map 中。

类型声明和插入函数如下所示:

import qualified Data.IntMap.Strict as M
import Data.UUID (UUID)
import Control.Monad.State
import System.Random

type StrMap = M.IntMap String
type MType = State StdGen

data MyType = MyType {
    uuid :: UUID,
    strs :: StrMap
} deriving (Show)

create :: StrMap -> MType MyType
create pm = do
    state <- get
    let (uuid, newState) = random state
    put newState
    return $ MyType uuid pm

strsSize :: MyType -> Int
strsSize e = M.size $ strs e

addStr :: MyType -> String -> MyType
addStr e p = e { strs = M.insert (strsSize e) p $ strs e }

Map 中的顺序键很重要,因此 [0, 1, 3] 是不可接受的。 我试图使用 HSpec 和 QuickCheck 来测试它:

main :: IO ()
main = hspec spec

spec :: Spec
spec = describe "Creation and update" $ do
    QuickCheck.prop "Check map addition" $ do
        \xs str -> monadicIO $ do
            state <- run(getStdGen)
            let (result, newState) = runState (create xs) state
            run(setStdGen newState)
            let result' = addStr result str
            assert $ (strsSize result) + 1 == strsSize result' -- fails here

问题是 QuickCheck 生成随机键,我不确定如何强制它为地图生成顺序键。缺少序列的问题是函数 addStr 可能会在重复键的情况下覆盖值,这是不可取的行为。


更新

感谢大家的帮助!经过长时间的讨论和某种思考后,我得出了以下解决方案:

spec :: Spec
spec = describe "Creation and update" $ do
    QuickCheck.prop "Check map addition" $ do
        \xs str -> not (null xs) Property.==> monadicIO $ do
            state <- run(getStdGen)
            let mp = M.fromList $ zip [0..(length xs)] xs
            let (result, newState) = runState (create mp) state
            run(setStdGen newState)
            let result' = addStr result str
            assert $ (strsSize result) + 1 == strsSize result'

基本上,我必须生成一些随机的字符串集,然后将它们手动转换为地图。它可能不是最优雅的解决方案,但它可以根据需要工作。

您可以使用 QuickCheck 生成完全任意的数据,然后从中构建满足不变量的数据(通过正在测试的系统外部的一些您认为正确的方法。

这种情况下的不变量给出为 "keys must be contiguous",但实际上是 "keys must be contiguous and start from 0"。这已经足够了,但比必要的还要多。 addStr 所需的最小不变量是 "the map must not contain a key that is the size of the map",因为这是我们打算插入的键。通过简化约束,我们也让它更容易满足:我们可以生成一个任意映射(其中可能包含坏键),然后删除坏键,给出一个令人满意的映射。

我还会注意到 UUID(以及生成它的机制,这需要 State,也许 IO)与正在测试的 属性 无关。这意味着我们可以用我们周围的任何 UUID 构造 MyType (比如包提供的 nil UUID)并避免单子的东西:

spec :: Spec
spec = describe "Creation and update" $ do
  QuickCheck.prop "Check map addition" $ do
    \strmap -> -- we don't actually care what the String being inserted is for this test
      let myType = MyType UUID.nil (M.delete (M.size strmap) strmap) -- Enforce the invariant
      in assert $ strsSize (addStr myType "") = strsSize myType + 1

如果你愿意,你也可以为 MyType 创建一个 Arbitrary 的实例来做这样的事情,或者满足更强的不变量(其他测试可能需要) .我会把它留给你作为练习,但如果你在尝试时遇到困难,请随时提出更多问题。