Search code examples
haskelltypestypeclassassertionshspec

Testing custom data types which do not implement Eq


Lets say I have the following custom data type:

data Animal = Characteristics [Char] (Set.Set [Char])

and some function

checkAnimalType :: [Char] -> Animal -> [Animal]

now I'm trying to write hspec tests for this like so:

describe "checkAnimalType" $ do
      it "returns a list of animals" $ do
        (checkAnimalType ["foo", "coo", "doo", "bar", "moo"](Characteristics "foo" $ Set.fromList(["foo", "coo"]))) $ `shouldBe` [(Characteristics "foo" $ Set.fromList(["cockadoodledoo"]))]

this fails with:

No instance for (Eq Animal) arising from a use of ‘shouldBe’

My question is, is it possible to temporarily, within the scope of the tests, implement the Eq typeclass on Animal? Or is there a better way to do this?


Solution

  • My question is, is it possible to temporarily, within the scope of the tests, implement the Eq typeclass on Animal?

    Within the scope of the module, sure. But if that module gets imported, you're going to leak the instance into other modules. That's why creating instances is only recommended in the same module the data type has been defined, or where the class has been defined. Otherwise you end up with orphan instances.

    Or is there a better way to do this?

    Is it even remotely possible that the user want to compare characteristics? Then derive Eq. It's the cleanest way. Also, you're going to need a Show instance, so you're probably deriving something already:

    data Animal = Characteristics [Char] (Set.Set [Char]) deriving (Show, Eq)
    

    If you cannot change the original source you can still use -XStandaloneDeriving to derive the instance in another module (see orphan instances above, though).

    However if you actually want to use some special Eq test you can either fiddle around with newtype wrappers, or simply write your own combinator:

    -- newtype variant
    newtype TAnimal = TAnimal Animal
    instance Eq TAnimal where ...
    instance Show TAnimal where...
    
    animalShouldBe :: Animal -> Animal -> Expectation
    animalShouldBe = shouldBe `on` TAnimal
    
    -- custom operator variant
    withShouldBe :: (Show a) => (a -> a -> Bool) -> a -> a -> Expectation
    withShouldBe f a e = unless (f a e) $ expectationFailure msg
     where msg = "expected: " ++ show e ++ ", but got " ++ show a
    
     animalShouldBe = withShouldBe animalEqualityTest
    
    -- Fun fact: shouldBe = withShouldBe (==)