haskellhspechakyll

Testing with Hakyll's MonadMetadata


For our Hakyll codebase, I've written a few helper methods and have started adding some HSpec unit tests around newer ones e.g.:

-- | Reject an item unless @fieldName@ is set to "true"
unlessEnabled :: MonadMetadata m
              => String
              -> Item a
              -> m Bool
unlessEnabled fieldName item = do
    maybeValue <- getMetadataBool (itemIdentifier item) fieldName
    return $ maybe True not maybeValue

-- | Try to look up a boolean field ("true" maps to @Just True@)
getMetadataBool :: MonadMetadata m
                => Identifier
                -> String
                -> m (Maybe Bool)
getMetadataBool ident name = do
    maybeString <- getMetadataField ident name
    return $ ((== "true") . map toLower) <$> maybeString

Now creating an Item or Identifier for testing is easy enough, but I'm not sure where to go with the MonadMetadata when running an Hspec.

I've seen testCompiler which feels like it might be copyable / useful (Compiler has a MonadMetadata instance), but I'm out of my Haskell-depth here...


Solution

  • Eventually (and thanks to @Bergi's suggestions) I got something working, and then tidied up:

    {-# LANGUAGE DeriveFunctor #-}
    {-# LANGUAGE GeneralizedNewtypeDeriving #-}
    
    type KeyedValuesOf a = [(String, a)]
    
    -- | Create Metadata from basic list of key-values -- stolen from Hakyll itself.
    meta :: Yaml.ToJSON a => KeyedValuesOf a -> Metadata
    meta pairs = HMS.fromList [(T.pack k, Yaml.toJSON v) | (k, v) <- pairs]
    
    -- | A 'Reader' class of our own, with a specialised environment
    --   for key-value metadata pairs.
    newtype MonadMetadataReader a =
        MonadMetadataReader {runMonadMetadataReader :: KeyedValuesOf String -> a}
            deriving (Functor, Applicative, Monad)
    
    -- | Simple 'Metadata' holder.
    -- There are never any matches found,
    -- and all metadata (for all items)
    -- comes from from the Reader-like environment at setup.
    instance MonadMetadata MonadMetadataReader where
        getMetadata identifier = MonadMetadataReader meta
    
        getMatches pattern = return []
    

    Which allowed simple unit tests like:

    it "enables for a value of \"false\"" $ do
        let enabled = runMonadMetadataReader (unlessEnabled "key" item) [("key", "false")]
        enabled `shouldBe` True
    
    it "disables for a value of \"true\"" $ do
        let enabled = runMonadMetadataReader (unlessEnabled "key" item) [("key", "true")]
        enabled `shouldBe` False
    
    it "enables if no metadata found" $ do
        let enabled = runMonadMetadataReader (unlessEnabled "key" item) []
        enabled `shouldBe` True