MyNixOS website logo
Description

Mock library for test in Haskell.

mockcat is a mock library for testing Haskell.

mockcat provides monad type class generation and stub and verification functions.

Stub functions can return values of pure types as well as values of monad types.

For more please see the README on GitHub at https://github.com/pujoheadsoft/mockcat#readme

🐈Mocking library for Haskell🐈‍

Test

日本語版 README はこちら

Overview

mockcat is a simple and flexible mocking library.

There are two main things you can do with mocks:

  1. Create stub functions
  2. Verify whether the stub functions were applied as expected

You can create two types of mocks:

  1. Mocks for monad type classes
  2. Mocks for functions

1 The monad type classes refer to classes like the following:

class Monad m => FileOperation m where
  readFile :: FilePath -> m Text
  writeFile :: FilePath -> Text -> m ()

2 The functions refer to regular functions like the following:
(You can mock functions wrapped in a monad like IO (), as well as constant functions)

calc :: Int -> Int
echo :: String -> IO ()
constantValue :: String

Mock of monad type class

Example usage

For example, suppose the following monad type class FileOperation and a function operationProgram that uses FileOperation are defined.

class Monad m => FileOperation m where
  readFile :: FilePath -> m Text
  writeFile :: FilePath -> Text -> m ()

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

You can generate a mock of the typeclass FileOperation by using the makeMock function as follows
makeMock [t|FileOperation|]

Then following two things will be generated:

  1. a MockT instance of typeclass `FileOperation
  2. a stub function based on a function defined in the typeclass FileOperation
    Stub functions are created as functions with _ prefix added to the original function.
    In this case, _readFile and _writeFile are generated.

Mocks can be used as follows.

spec :: Spec
spec = do
  it "Read, and output files" do
    result <- runMockT do
      _readFile ("input.txt" |> pack "content")
      _writeFile ("output.txt" |> pack "content" |> ())
      operationProgram "input.txt" "output.txt"

    result `shouldBe` ()

Stub functions are passed arguments that are expected to be applied to the function, concatenated by |>.
The last value of |> is the return value of the function.

Mocks are run with runMockT.

Verification

After execution, the stub function is verified to see if it is applied as expected.
For example, the expected argument of the stub function _writeFile in the above example is changed from "content" to "edited content".

result <- runMockT do
  _readFile ("input.txt" |> pack "content")
  _writeFile ("output.txt" |> pack "edited content" |> ())
  operationProgram "input.txt" "output.txt"

If you run the test, the test will fail and you will get the following error message.

uncaught exception: ErrorCall
function `_writeFile` was not applied to the expected arguments.
  expected: "output.txt", "edited content"
  but got: "output.txt", "content"

Suppose also that you did not use the stub function corresponding to the function you are using in your test case, as follows

result <- runMockT do
  _readFile ("input.txt" |> pack "content")
  -- _writeFile ("output.txt" |> pack "content" |> ())
  operationProgram "input.txt" "output.txt"

Again, when you run the test, the test fails and you get the following error message.

no answer found stub function `_writeFile`.

Verify the number of times applied

For example, suppose you want to write a test for not applying _writeFile if it contains a specific string as follows.

operationProgram inputPath outputPath = do
  content <- readFile inputPath
  unless (pack "ngWord" `isInfixOf` content) $
    writeFile outputPath content

This can be accomplished by using the applyTimesIs function as follows.

import Test.MockCat as M
...
it "Read, and output files (contain ng word)" do
  result <- runMockT do
    _readFile ("input.txt" |> pack "contains ngWord")
    _writeFile ("output.txt" |> M.any |> ()) `applyTimesIs` 0
    operationProgram "input.txt" "output.txt"

  result `shouldBe` ()

You can verify that it was not applied by specifying 0.

Or you can use the neverApply function to accomplish the same thing.

result <- runMockT do
  _readFile ("input.txt" |> pack "contains ngWord")
  neverApply $ _writeFile ("output.txt" |> M.any |> ())
  operationProgram "input.txt" "output.txt"

M.any is a parameter that matches any value.
This example uses M.any to verify that the writeFile function does not apply to any value.

As described below, mockcat provides a variety of parameters other than M.any.

Mock constant functions

mockcat can also mock constant functions.
Let's mock MonadReader and use the ask stub function.

data Environment = Environment { inputPath :: String, outputPath :: String }

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

makeMock [t|MonadReader Environment|]]

spec :: Spec
spec = do
  it "Read, and output files (with MonadReader)" do
    r <- runMockT do
      _ask (Environment "input.txt" "output.txt")
      _readFile ("input.txt" |> pack "content")
      _writeFile ("output.txt" |> pack "content" |> ())
      operationProgram
    r `shouldBe` ()

Now let's try to avoid using ask.

operationProgram = do
  content <- readFile "input.txt"
  writeFile "output.txt" content

Then the test run fails and you will see that the stub function was not applied.

It has never been applied function `_ask`

Rename stub functions

The prefix and suffix of the generated stub functions can optionally be changed.
For example, the following will generate the functions stub_readFile_fn and stub_writeFile_fn.

makeMockWithOptions [t|FileOperation|] options { prefix = "stub_", suffix = "_fn" }

If no options are specified, it defaults to _.

Code generated by makeMock

Although you do not need to be aware of it, the makeMock function generates the following code.

-- MockT instance
instance (Monad m) => FileOperation (MockT m) where
  readFile :: Monad m => FilePath -> MockT m Text
  writeFile :: Monad m => FilePath -> Text -> MockT m ()

_readFile :: (MockBuilder params (FilePath -> Text) (Param FilePath), Monad m) => params -> MockT m ()
_writeFile :: (MockBuilder params (FilePath -> Text -> ()) (Param FilePath :> Param Text), Monad m) => params -> MockT m ()

Mocking functions

In addition to mocking monad type classes, mockcat can also mock regular functions.
Unlike monad type mocks, the original function is not required.

Example usage

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "usage example" do
    -- create a mock (applying "value" returns the pure value True)
    mock <- createMock $ "value" |> True

    -- extract a stub function from a mock
    let stubFunction = stubFn mock

    -- verify the result of applying the function
    stubFunction "value" `shouldBe` True

    -- verify that the expected value ("value") has been applied
    mock `shouldApplyTo` "value"

```

## Stub functions
To create a stub function directly, use the `createStubFn` function.  
If you don't need verification, you can use this one.
```haskell
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "can generate stub functions" do
    -- generate
    f <- createStubFn $ "param1" |> "param2" |> pure @IO ()

    -- apply
    actual <- f "param1" "param2"

    -- Verification
    actual `shouldBe` ()
```
The `createStubFn` function is passed a sequence of `|>` arguments that the function is expected to apply.
The last value of `|>` is the return value of the function.

If the stub function is applied to an argument it is not expected to be applied to, an error is returned.
```console
Uncaught exception: ErrorCall
Expected arguments were not applied to the function.
  expected: "value"
  but got: "valuo"

Named Stub Functions

You can name stub functions.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "named stub" do
    f <- createNamedStubFun "named stub" $ "x" |> "y" |> True
    f "x" "z" `shouldBe` True

The error message printed when a stub function is not applied to an expected argument will include this name.

uncaught exception: ErrorCall
Expected arguments were not applied to the function `named stub`.
  expected: "x","y"
  but got: "x","z"

Constant stub functions

To create a stub function that returns a constant, use the createConstantMock or createNamedConstantMock function.

spec :: Spec
spec = do
  it "createConstantMock" do
    m <- createConstantMock "foo"
    stubFn m `shouldBe` "foo"
    shouldApplyToAnything m

  it "createNamedConstantMock" do
    m <- createNamedConstantMock "const" "foo"
    stubFn m `shouldBe` "foo""
    shouldApplyToAnything m

Flexible stub functions

Flexible stub functions can be generated by giving the createStubFn function a conditional expression rather than a concrete value.
This can be used to return expected values for arbitrary values or strings that match a specific pattern.
This is also true for the stub function when generating a mock of a monad type.

any

any matches any value.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat
import Prelude hiding (any)

spec :: Spec
spec = do
  it "any" do
    f <- createStubFn $ any |> "return value"
    f "something" `shouldBe` "return value"

Since a function with the same name is defined in Prelude, we use import Prelude hiding (any).

Condition Expressions

Using the expect function, you can handle arbitrary condition expressions.
The expect function takes a condition expression and a label.
The label is used in the error message if the condition is not met.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "expect" do
    f <- createStubFn $ expect (> 5) "> 5" |> "return value"
    f 6 `shouldBe` "return value"

Condition Expressions without Labels

expect_ is a label-free version of expect.
The error message will show [some condition].

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "expect_" do
    f <- createStubFn $ expect_ (> 5) |> "return value"
    f 6 `shouldBe` "return value"

Condition Expressions using Template Haskell

Using expectByExp, you can handle condition expressions as values of type Q Exp.
The error message will include the string representation of the condition expression.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TemplateHaskell #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "expectByExpr" do
    f <- createStubFn $ $(expectByExpr [|(> 5)|]) |> "return value"
    f 6 `shouldBe` "return value"

Stub functions that return different values for each argument applied

By applying the createStubFn function to a list of x |> y format, you can create a stub function that returns a different value for each argument you apply.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat
import Prelude hiding (and)

spec :: Spec
spec = do
  it "multi" do
    f <-
      createStubFn
        [ "a" |> "return x",
          "b" |> "return y"
        ]
    f "a" `shouldBe` "return x"
    f "b" `shouldBe` "return y"

Stub functions that return different values when applied to the same argument

When the createStubFn function is applied to a list of x |> y format, with the same arguments but different return values, you can create stub functions that return different values when applied to the same arguments.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat
import GHC.IO (evaluate)

spec :: Spec
spec = do
  it "Return different values for the same argument" do
    f <- createStubFn [
        "arg" |> "x",
        "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.

Verify that expected arguments are applied

The shouldApplyTo function can be used to verify that a stub function has been applied to the expected arguments.
If you want to verify this, you need to create a mock with the createMock function instead of the createStubFn function.
In this case, stub functions are taken from the mock with the stubFn function.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "stub & verify" do
    -- create a mock
    mock <- createMock $ "value" |> True
    -- stub function
    let stubFunction = stubFn mock
    -- assert
    stubFunction "value" `shouldBe` True
    -- verify
    mock `shouldApplyTo` "value"

Note

The record that it has been applied is made at the time the return value of the stub function is evaluated.
Therefore, verification must occur after the return value is evaluated.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "Verification does not work" do
    mock <- createMock $ "expect arg" |> "return value"
    -- Apply arguments to stub functions but do not evaluate values
    let _ = stubFn mock "expect arg"
    mock `shouldApplyTo` "expect arg"
uncaught exception: ErrorCall
Expected arguments were not applied to the function.
  expected: "expect arg"
  but got: Never been called.

Verify the number of times the stub function was applied to the expected argument

The number of times a stub function is applied to an expected argument can be verified with the shouldApplyTimes function.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "shouldApplyTimes" do
    m <- createMock $ "value" |> True
    print $ stubFn m "value"
    print $ stubFn m "value"
    m `shouldApplyTimes` (2 :: Int) `to` "value"

Verify that a function has been applied to something

You can verify that a function has been applied to something with the shouldApplyToAnything function.

Verify the number of times a function has been applied to something

The number of times a function has been applied to something can be verified with the shouldApplyTimesToAnything function.

Verify that stub functions are applied in the expected order

The shouldApplyInOrder function can be used to verify that the order in which they were applied is the expected order.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "shouldApplyInOrder" do
    m <- createMock $ any |> True |> ()
    print $ stubFn m "a" True
    print $ stubFn m "b" True
    m
      `shouldApplyInOrder` [ "a" |> True,
                             "b" |> True
                           ]

Verify that they were applied in the expected order (partial match)

While the shouldApplyInOrder function verifies the exact order of application,
The shouldApplyInPartialOrder function allows you to verify that the order of application is partially matched.

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.MockCat

spec :: Spec
spec = do
  it "shouldApplyInPartialOrder" do
    m <- createMock $ any |> True |> ()
    print $ stubFn m "a" True
    print $ stubFn m "b" True
    print $ stubFn m "c" True
    m
      `shouldApplyInPartialOrder` [ "a" |> True,
                                    "c" |> True
                                  ]
Metadata

Version

0.3.1.0

License

Platforms (75)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows