hedgehogqa / haskell-hedgehog-classes

Hedgehog will eat your typeclass bugs
BSD 3-Clause "New" or "Revised" License
56 stars 18 forks source link

Integration with hspec #41

Open chshersh opened 2 years ago

chshersh commented 2 years ago

It would be nice if hedgehog-classes could be integrated with other tasting frameworks, e.g. hspec.

Currently, when I run lawsCheck inside the hspec test suite, I see the following slightly messy output:

Tests
  Lecture 3
    Gold
      Laws: Semigroup [ ]Semigroup: Associativity    ✓ <interactive> passed 100 tests.
Semigroup: Concatenation    ✓ <interactive> passed 100 tests.
Semigroup: Times    ✓ <interactive> passed 100 tests.
      Laws: Semigroup [✔]
      Laws: Monoid [ ]Monoid: Left Identity    ✓ <interactive> passed 100 tests.
Monoid: Right Identity    ✓ <interactive> passed 100 tests.
Monoid: Associativity    ✓ <interactive> passed 100 tests.
Monoid: Concatenation    ✓ <interactive> passed 100 tests.
      Laws: Monoid [✔]

Here is my relevant test-suite code:

    describe "Gold" $ do
        it "Laws: Semigroup" $
            lawsCheck (semigroupLaws genGold) `shouldReturn` True
        it "Laws: Monoid" $
            lawsCheck (monoidLaws genGold) `shouldReturn` True

I see that hedgehog already has integration with hspec in the form of hspec-hedgehog package. The integration is done my providing relevant instances to the PropertyT data type.

So, I wonder, if hedgehog-classes can expose PropertyT of the underlying laws to be used with hspec 🤔 Or in any other way 🙂

chessai commented 1 year ago

I would take a PR for this.

sol commented 9 months ago

This can be done with

import Test.Hspec
import Test.Hspec.Hedgehog
import Control.Monad
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Hedgehog.Classes
import Hedgehog.Internal.Property

satisfies :: Gen a -> (Gen a -> Laws) -> Spec
satisfies gen laws = do
  describe (className <> " instance") $ do
    forM_ properties $ \ (name, p) -> do
      it ("satisfies " <> name) (propertyTest p)
  where
    Laws className properties = laws gen

which then can be used with e.g.:

main :: IO ()
main = hspec spec

arbitraryString :: Gen [Char]
arbitraryString = Gen.string (Range.constant 0 10) (Gen.unicodeAll)

spec :: Spec
spec = do
  describe "String" $ do
    arbitraryString `satisfies` eqLaws
    arbitraryString `satisfies` monoidLaws

(note that this ignores the propertyConfig and instead uses the settings provided by hspec)

chessai commented 7 months ago

Also, for satisfies1, I think you can do the following:

-- needs AllowAmbiguousTypes, TypeApplications, ScopedTypeVariables, QuantifiedConstraints, RankNTypes

satisfies1 :: forall c f. (c f, forall x. Eq x => Eq (f x), forall x. Show x => Show (f x))
  => (forall a. Gen a -> Gen (f a))
  -> ((c f, forall x. Eq x => Eq (f x), forall x. Show x => Show (f x)) => (forall x. Gen x -> Gen (f x)) -> Laws)
  -> Spec
satisfies1 gen laws = case laws gen of
  Laws className properties -> do
    describe (className <> " instance") $ do
      forM_ properties $ \(name, p) -> do
        it ("satisfies " <> name) (propertyTest p)

genList :: Gen a -> Gen [a]
genList = Gen.list (Range.linear 0 6)

foo :: Spec ()
foo = satisfies1 @Applicative @[] genList applicativeLaws

and you could basically do the same thing for satisfies2.