packages feed

mockcat-1.4.1.1: test/Test/MockCat/ExampleSpec.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-deprecations #-}
{-# OPTIONS_GHC -fno-full-laziness #-}
{-# OPTIONS_GHC -fno-cse #-}

module Test.MockCat.ExampleSpec (spec) where

import Test.Hspec
import Test.MockCat
import Prelude hiding (writeFile, readFile, and, any, not, or)
import GHC.IO (evaluate)
import Data.Text hiding (any)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Test.MockCat.SharedSpecDefs

makeAutoLiftMock [t|FileOperation|]

operationProgram ::
  FileOperation m =>
  FilePath ->
  FilePath ->
  m ()
operationProgram inputPath outputPath = do
  content <- readFile inputPath
  writeFile outputPath content

echoProgram :: Teletype m => m ()
echoProgram = do
  i <- readTTY
  case i of
    "" -> pure ()
    _  -> writeTTY i >> echoProgram

makeMock [t|Teletype|]

class Monad m => StrictTest m where
  strictFunc :: String -> m ()

instance StrictTest IO where
  strictFunc _ = pure ()

makeMock [t|StrictTest|]

spec :: Spec
spec = do
  it "Default makeMock is strict (requires pure)" do
    r <- runMockT do
      _strictFunc $ "arg" ~> pure @IO ()
      strictFunc "arg"
    r `shouldBe` ()

  it "function arg" do
    let
      f :: String -> String -> String
      f a b = a <> b
    -- Example of explicit type application using any @type
    stub <- mock $ "a" ~> any @(String -> String -> String) ~> True
    stub "a" f `shouldBe` True

  it "function arg (inferred)" do
    let
      f :: String -> String -> String
      f a b = a <> b
    -- Example of relying on type inference
    stub <- mock $ "a" ~> any ~> True
    stub "a" f `shouldBe` True

  it "function arg2" do
    let
      -- Do not allow optimization to remove duplicates.
      {-# NOINLINE f #-}
      f :: String -> String -> String
      f a b = a <> b
      
      {-# NOINLINE g #-}
      g :: String -> String -> String
      g a b = a <> b

    stub <- mock $ "a" ~> f ~> g ~> True

    -- Verify that calling with wrong function order raises an error
    evaluate (stub "a" g f) `shouldThrow` anyErrorCall

  it "echo1" do
    result <- runMockT do
      _readTTY (pure @IO "") `expects` called once
      echoProgram
    result `shouldBe` ()

  it "echo2" do
    result <- runMockT do
      _readTTY (do
        onCase $ pure @IO "a"
        onCase $ pure @IO "")
        `expects` called (times 2)

      _writeTTY $ "a" ~> pure @IO ()
      echoProgram
    result `shouldBe` ()

  it "echo3" do
    result <- runMockT do
      _readTTY $ casesIO ["a", ""]
      _writeTTY $ "a" ~> pure @IO ()
      echoProgram
    result `shouldBe` ()

  it "echo4" do
    result <- runMockT do
      _readTTY $ cases [ pure @IO "a", pure @IO "" ]
      _writeTTY $ "a" ~> pure @IO ()
      echoProgram
    result `shouldBe` ()

  it "read & write" do
    result <- runMockT do
      _readFile $ "input.txt" ~> pack "Content"
      _writeFile $ "output.text" ~> pack "Content" ~> ()
      operationProgram "input.txt" "output.text"

    result `shouldBe` ()

  it "stub" do
    -- create a stub function
    stubFn <- mock $ "value" ~> True
    -- assert
    stubFn "value" `shouldBe` True

  it "stub & verify" do
    -- create a mock
    mockFn <- mock $ "value" ~> True
    -- assert
    mockFn "value" `shouldBe` True

  it "how to use" do
    f <- mock $ "param1" ~> "param2" ~> pure @IO ()
    actual <- f "param1" "param2"
    actual `shouldBe` ()

  it "named stub" do
    f <- mock (label "named stub") $ "x" ~> "y" ~> True
    f "x" "y" `shouldBe` True

  it "mock returns monadic stub (IO)" do
    f <- mock $ "a" ~> (11 :: Int) ~> pure @IO False
    f "a" (11 :: Int) `shouldReturn` False

  it "mock returns monadic stub (MaybeT)" do
    mm <- runMaybeT do
      -- create a mock inside MaybeT
      mock $ True ~> pure @(MaybeT IO) False
    case mm of
      Nothing -> expectationFailure "mock returned Nothing"
      Just f -> do
        -- f :: Bool -> MaybeT IO Bool
        res <- runMaybeT $ f True
        res `shouldBe` Just False

  it "named mock" do
    f <- mock (label "mock") $ "value" ~> "a" ~> True
    f "value" "a" `shouldBe` True

  it "stub function" do
    f <- mock $ "value" ~> True
    f "value" `shouldBe` True

  it "any" do
    f <- mock $ any ~> "return value"
    f "something" `shouldBe` "return value"

  it "expect" do
    -- Explicit type annotations like (4 :: Int) are required because the polymorphism 
    -- of numeric literals and operators makes the type of Param ambiguous.
    f <- mock $ expect (> (5 :: Int)) "> 5" ~> "return value"
    f 6 `shouldBe` "return value"

  it "expect_" do
    f <- mock $ expect_ (> (5 :: Int)) ~> "return value"
    f 6 `shouldBe` "return value"

  it "expectByExpr" do
    f <- mock $ $(expectByExpr [|(> (5 :: Int))|]) ~> "return value"
    f 6 `shouldBe` "return value"

  it "multi" do
    f <- mock do
      onCase $ "a" ~> "return x"
      onCase $ "b" ~> "return y"

    f "a" `shouldBe` "return x"
    f "b" `shouldBe` "return y"

  it "Return different values for the same argument" do
    f <- mock $ do
      onCase $ "arg" ~> "x"
      onCase $ "arg" ~> "y"

    -- Do not allow optimization to remove duplicates.
    v1 <- evaluate $ f "arg"
    v2 <- evaluate $ f "arg"
    v3 <- evaluate $ f "arg"
    v1 `shouldBe` "x"
    v2 `shouldBe` "y"
    v3 `shouldBe` "y" -- After the second time, “y” is returned.