0

tl;dr

I'd like to understand how to use Test.QuickCheck to test functions whose arguments are passed to smart constructors and/or assumed as coming out of a smart constructor.

Long version

In a module I have a user defined type like this:

newtype SpecialStr = SpecialStr { getStr :: String }

As the name implies, SpecialStr is quite special, in that the wrapped String is not "any" string, but it has to satisfy some properties. To enforce this, I don't export the value constructor from the module, but a smart constructor:

specialStr :: String -> SpecialStr
specialStr str = assert (isValid str) $ SpecialStr str
  where
    isValid :: String -> Bool
    isValid = and . map (`elem` "abcdef") -- this is just an example logic

Naturally, I've defined some functions that operate with these SpecialStrs, such as

someFunc :: String -> [SpecialStr]
someFunc str = -- uses smart constructor
someOtherFunc :: (Int, SpecialStr) -> Whatever
someOtherFunc = -- assumes the input has been created via the smart constructor, i.e. assumes it's valid

where maybe someFunc is fed with a String, and then the outcoming [SpecialStr] is zipped with [1..] and the result is fed to someOtherFunc, just to make a random example.

Now my question is: how do I test these functions using QuickCheck?

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 4
    if you can write an [`Arbitrary`](https://hackage.haskell.org/package/QuickCheck-2.14.2/docs/Test-QuickCheck.html#t:Arbitrary) instance for your `SpecialStr` then you can use it as *inputs* in your property-tests – Random Dev Apr 03 '21 at 16:29
  • @Carsten, I'm having a hard time with that. I read the relevant chapter from [RWH](http://book.realworldhaskell.org/read/testing-and-quality-assurance.html), but it is a bit old. For instance, `generate` doesn't work anymore as used in the book. So an answer with an example would be useful. – Enlico Apr 03 '21 at 17:41

1 Answers1

2

Got a smart constructor? Write a smart Arbitrary instance.

instance Arbitrary SpecialStr where
    arbitrary = SpecialStr <$> listOf (choose ('a', 'f'))
    shrink (SpecialStr s) = SpecialStr (shrinkList (enumFromTo 'a') s)

Then just write your properties as usual.

-- check that someOtherFunc is hypermonotonic in the SpecialStrings
quickCheck (\n1 n2 s1 s2 -> someOtherFunc (n1, min s1 s2) <= someOtherFunc (n2, max s1 s2))

...or whatever.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • I suspect the question is how to write an Arbitrary instance in a separate test module if the module that defines `SpecialStr` doesn't export the `SpecialStr` data constructor. – ivan Jun 04 '22 at 17:58
  • 1
    @ivan It is generally considered best practice to put instances next to either the data definition or the class definition to avoid orphan instances. Still, if you're okay with orphan instances, then `arbitrary = specialStr <$> ...` works fine and doesn't use anything private. `shrink` probably can't be sanely done with the API shown in the question, but then again presumably the real API is bigger. – Daniel Wagner Jun 04 '22 at 22:39