mockcat 1.4.1.1 → 1.5.0.0
raw patch · 16 files changed
+326/−155 lines, 16 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Test.MockCat.Param: infixr 1 ~>
+ Test.MockCat.Internal.Builder: incrementCount :: Int -> [(Int, Int)] -> [(Int, Int)]
+ Test.MockCat.Internal.Builder: lookupCount :: Int -> [(Int, Int)] -> Int
+ Test.MockCat.Internal.Types: [caseInvocationCounts] :: InvocationRecord params -> [(Int, Int)]
+ Test.MockCat.Param: [ResponseSequence] :: forall v. NonEmpty (Param v) -> Param v
+ Test.MockCat.Param: andThen :: AndThen params => params -> ResponseOf params -> params
+ Test.MockCat.Param: class AndThen params
+ Test.MockCat.Param: infixl 1 `andThen`
+ Test.MockCat.Param: infixr 2 ~>
+ Test.MockCat.Param: instance (Test.MockCat.Param.AndThen rest, Test.MockCat.Param.ResponseOf (Test.MockCat.Param.Param a Test.MockCat.Cons.:> rest) GHC.Types.~ Test.MockCat.Param.ResponseOf rest) => Test.MockCat.Param.AndThen (Test.MockCat.Param.Param a Test.MockCat.Cons.:> rest)
+ Test.MockCat.Param: instance (Test.MockCat.Param.ToParamResult r, Test.MockCat.Param.Normalize r GHC.Types.~ Test.MockCat.Param.Param r) => Test.MockCat.Param.AndThen (Test.MockCat.Param.Param a Test.MockCat.Cons.:> Test.MockCat.Param.Param r)
+ Test.MockCat.Param: returnValueAt :: (ProjectionReturn params, ReturnOf params ~ Param r) => Int -> params -> r
- Test.MockCat.Internal.Types: InvocationRecord :: InvocationList params -> InvocationCounts params -> InvocationRecord params
+ Test.MockCat.Internal.Types: InvocationRecord :: InvocationList params -> InvocationCounts params -> [(Int, Int)] -> InvocationRecord params
Files
- CHANGELOG.md +9/−0
- README-ja.md +78/−59
- README.md +73/−51
- mockcat.cabal +9/−8
- src/Test/MockCat.hs +2/−2
- src/Test/MockCat/Internal/Builder.hs +25/−20
- src/Test/MockCat/Internal/Types.hs +2/−1
- src/Test/MockCat/Mock.hs +5/−3
- src/Test/MockCat/Param.hs +59/−1
- test/Property/AdditionalProps.hs +5/−1
- test/Support/ParamSpec.hs +1/−0
- test/Test/MockCat/ExampleSpec.hs +8/−1
- test/Test/MockCat/Internal/MockRegistrySpec.hs +2/−3
- test/Test/MockCat/MockSpec.hs +39/−4
- test/Test/MockCat/Readme/MatcherSpec.hs +1/−0
- test/Test/MockCat/ShouldBeCalledMockMSpec.hs +8/−1
CHANGELOG.md view
@@ -6,6 +6,15 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## [Unreleased]++## [1.5.0.0] - 2026-09-12+### Added+- Added `andThen` for declaring consecutive responses on a single mock case.++### Changed+- **Breaking Change**: `onCase` definitions now use first-match-wins semantics, matching Haskell pattern matching. Each selected case maintains its own consecutive-response position, and repeats its final response after exhaustion. Repeated overlapping `onCase` definitions no longer form an implicit response sequence; use `andThen` instead.+ ## [1.4.1.1] - 2026-01-17 ### Fixed - **Test Suite Stability**: Added GHC optimization pragmas (`-fno-cse`, `-fno-full-laziness`) to ExampleSpec.hs and WithMockSpec.hs to prevent test failures on certain GHC builds (notably unofficial Ubuntu 24.04 bindists for GHC 9.2.8).
README-ja.md view
@@ -1,6 +1,6 @@ <div align="center"> <img src="https://raw.githubusercontent.com/pujoheadsoft/mockcat/main/logo.png" width="600px" alt="Mockcat Logo">- <h1>Declarative mocking with a single arrow <code>~></code></h1>+ <h1>Stub Haskell functions. Verify calls when needed.</h1> </div> <div align="center">@@ -13,8 +13,10 @@ </div> -**Mockcat** は、Haskell のための直感的で宣言的なモックライブラリです。+**Mockcat** は、Haskell のためのテストダブルライブラリです。 +スタブ関数 `stub` と、モック関数 `mock` を用意しています。 +スタブ関数はこれだけです: ```haskell -- スタブ: 「"a" が来たら True を返す」 let f :: String -> Bool@@ -22,11 +24,11 @@ f "a" -- => True ```--これだけです。`~>` の左に引数、右に戻り値を書くだけ。+`~>` の左に引数、右に戻り値を書くだけ。 検証は行わず、純粋な関数を返します。 -**呼び出しを検証したい** なら、`mock` と `expects` を使います:+呼び出しの検証が重要な場合は `mock` を使います。 +`expects` を使えば、実行前に期待値を宣言できます: ```haskell withMockIO $ do@@ -37,43 +39,42 @@ -- withMockIO のスコープ終了時に「1回呼ばれたか」がチェックされる ``` -> **使い分け:**-> - 値を返すだけなら `stub`(純粋、検証なし)-> - 呼び出しを検証したいなら `mock`(常に `expects` とセットで)+> **おすすめの使い方:**+> - 普通の値やラムダ式で十分なら、そのまま使う。+> - 決まった値を返せればよいなら `stub`(純粋、検証なし)。+> - 引数、回数、順序を検証するなら `mock`。 --- ## 概念と用語 (Concepts & Terminology) -Mockcat は、「実行時に検証を行いますが、定義時に『満たすべき条件』を宣言できる」という設計を採用しています。+Mockcat は、決まった値を返すことと、呼び出され方を検証することを分けて扱います。 * **Stub (スタブ)**:- テストを進めるために値を返すだけの存在。「どう呼ばれたか」に関心を持ちません。- `stub` 関数は完全に純粋な関数を返します。+ 期待する引数に対して決まった値を返すことができます。 + 呼び出し履歴を利用した検証が不要な場合に使います。 * **Mock (モック)**:- スタブの機能に加え、「期待通りに呼び出されたか」を記録・検証する存在。- `mock` 関数は、呼び出しを記録しながら値を返します。検証はテストの最後に行うことも、モック定義時に「この条件で呼ばれるはずだ」と宣言することも可能です(`expects` による宣言的検証)。-+ スタブの機能に加え呼び出しの履歴を持ち、「期待通りに呼び出されたか」を検証することができます。 + 検証は実行後に行うことも、実行前に「この条件で呼ばれるはずだ」と宣言することもできます。 --- ## Why Mockcat? -Haskell におけるモック記述を、できるだけ自然な形で行えるよう設計されています。--Mockcat は、**「アーキテクチャに依存せず、関数の "振る舞いと意図" を宣言的に記述できる」** モックライブラリです。+純粋なロジックは、値と関数で直接テストできるため、スタブやモックを必要としないでしょう。 +一方で、テストダブルが必要になる場面では、検証のための仕組みを必要とするはずです。 +MockCatは、そういう仕組みを提供するシンプルなライブラリです。 -「型クラス (MTL) を導入しないとテストできない」-「モックのために専用のデータ型を定義しなければならない」-(例: 型クラスを増やす/Service Handle 用のレコードを別途用意する、など)+Mockcat は、**特定のアーキテクチャに依存せず、関数の振る舞いと呼び出され方を宣言的に記述できます。** -そんな制約から解放されます。既存の関数をそのままモックし、設計が固まりきっていない段階でもテストを書き進めることができます。+普通の関数、`IO` を返す関数、引数として注入する関数、Service Handle のフィールド、+MTL / Capability 型クラスなど、既存の設計に合わせて利用できます。 **Mockcat は、テストのために設計を固定するのではなく、設計を試すためにテストを書けることを目指しています。** ### Before / After -Mockcat を使うことで、テスト記述は次のようになります。+Mockcat を使うことで、テストの記述は次のようになります。 | | **Before: 手書き...** 😫 | **After: Mockcat** 🐱✨ | | :--- | :--- | :--- |@@ -82,10 +83,10 @@ ### 主な特徴 -* **Haskell ネイティブな DSL**: 冗長なデータコンストラクタや専用の記法を覚えなくても、関数定義と同じ感覚 (`引数 ~> 戻り値`) で自然に記述できます。-* **アーキテクチャ非依存**: MTL (型クラス)、Service Handle (レコード)、あるいは純粋な関数。どのような設計パターンを採用していても、最小単位で導入可能です。+* **Haskell ネイティブな DSL**: 冗長なデータコンストラクタや専用の記法を覚えなくても、関数定義と同じ感覚 (`引数 ~> 戻り値`) でテストダブルを自然に記述できます。+* **アーキテクチャ非依存**: MTL (型クラス)、Service Handle (レコード)、あるいは関数。すでにある設計へ Mockcat が合わせます。 * **値ではなく「条件」で検証**: 引数が `Eq` インスタンスを持っていなくても問題ありません。値の一致だけでなく、「どのような性質を満たすべきか」という条件 (Predicate) で検証できます。-* **圧倒的に親切なエラー**: テスト失敗時、どこが違うのかを「構造差分」で表示します。+* **親切なメッセージ**: テスト失敗時、どこが違うのかを「構造差分」で表示します。 ```text function was not called with the expected arguments. @@ -100,7 +101,6 @@ ``` * **意図を導く型設計**: 型はあなたの記述を縛るものではなく、テストの意図(何を期待しているか)を自然に表現させるために存在します。 - --- ## クイックスタート@@ -158,11 +158,12 @@ ## 使い方ガイド (User Guide) -Mockcat は、テストの目的や環境に応じて 2 つの検証スタイルをサポートしています。+呼び出され方の検証が必要な場合、Mockcat では期待値を書くタイミングに応じて 2 つの検証スタイルを選べます。 -### 1. 宣言的な検証 (`withMock` (`withMockIO`) / `expects`) - [推奨]+### 1. 宣言的な検証 (`withMock` (`withMockIO`) / `expects`) -定義と同時に期待値を記述するスタイルです。スコープを抜ける時に自動的に検証が走ります。+定義と同時に期待値を記述するスタイルです。 +スコープを抜ける時に自動的に検証が走ります。 「定義」と「検証」を近くに書きたい場合に便利です。 ```haskell@@ -176,7 +177,7 @@ withMock $ do -- "Hello" に対して True を返すモックを定義 f <- mock ("Hello" ~> True)- `expects` called once+ `expects` called once -- 一度だけ呼ばれることを期待 -- 実行 let result = f "Hello"@@ -185,7 +186,8 @@ ``` #### `withMockIO`: IO テストの簡略化-`withMockIO` は `withMock` を IO に特化させたバージョンです。`liftIO` を使わずにモックコンテキスト内で直接 IO アクションを実行できます。+`withMockIO` は `withMock` を IO に特化させたバージョンです。 +`liftIO` を使わずにモックコンテキスト内で直接 IO アクションを実行できます。 ```haskell import Test.Hspec@@ -205,7 +207,7 @@ > [!IMPORTANT] > `expects`(宣言的検証)を使用する場合、モック定義部分は必ず **括弧 `(...)`** で囲んでください。-> 以前のバージョンで使用できた `$` 演算子 (`mock $ ... expected ...`) は、優先順位の関係でコンパイルエラーになります。+> 以前のバージョンで使用できた `$` 演算子 (`mock $ ... expects ...`) は、優先順位の関係でコンパイルエラーになります。 > > ❌ `mock $ any ~> True expects ...` > ✅ `mock (any ~> True) expects ...`@@ -216,14 +218,16 @@ > > ```haskell > runMockT do-> _readFile "config.txt" ~> pure "value"+> _readFile ("config.txt" ~> pure "value") > `expects` called once > ``` ### 2. 型クラスを使った設計でのモック (`makeMock`) -型クラスで依存を表現している設計(MTL スタイルや Capability パターン)において、-そのままテストに持ち込みたい場合に使います。Template Haskell を使って、型クラスからモックを自動生成します。+型クラスで依存を表現している設計(MTL スタイルや Capability パターン)において、そのままテストに持ち込みたい場合に使います。 +Template Haskell を使って、型クラスからモックを自動生成します。 +これは型クラスを使っている場合の選択肢です。 +テストのためだけに型クラスを導入する必要はありません。 ```haskell {-# LANGUAGE TemplateHaskell #-}@@ -273,8 +277,8 @@ ### 3. 関数のモックと事後検証 (`mock` / `shouldBeCalled`) -`withMock` (`withMockIO`) を使用せずに、特定の引数に対して値を返す関数を作ることもできます。-その場合は、事後検証 (`shouldBeCalled`) を組み合わせることになります。+`mock` を作成してテスト対象を実行した後、記録された呼び出しを `shouldBeCalled` で検証するスタイルです。 +実行後に検証を書く方が自然な場合に使います。 ```haskell import Test.Hspec@@ -322,7 +326,7 @@ ### 4. 柔軟な検証(マッチャー) -引数が `Eq` インスタンスを持っていなくても、あるいは特定の値に依存したくない場合でも、「どのような条件を満たすべきか」という**意図**で検証できます。+引数が `Eq` インスタンスを持っていなくても、あるいは特定の値に依存したくない場合でも、「どのような条件を満たすべきか」という**意図**で検証できます。 Mockcat は、値の一致だけでなく、関数の性質を検証するための**マッチャー**を提供しています。 #### 任意の値を許可 (`any`)@@ -337,7 +341,7 @@ #### 条件を指定して検証 (`when`) -任意の値ではなく、「条件(述語)」を使って検証できます。+任意の値ではなく、「条件(述語)」を使って検証できます。 `Eq` を持たない型(関数など)や、部分的な一致を確認したい場合に強力です。 ```haskell@@ -362,8 +366,9 @@ #### mock vs stub vs mockM の使い分け -使う関数は、テスト対象の性質に応じて選択するとよいでしょう。-細かい違いは以下の表を参照してください。+まず呼び出しの検証が必要かを判断します。 +普通の値や関数で十分ならそのまま使い、決まった応答だけが必要なら `stub` を使います。 +呼び出され方自体を検証する場合に `mock` または `mockM` を選びます。 | 関数 | 検証 (`shouldBeCalled`) | IO 依存 | 特徴 | | :--- | :---: | :---: | :--- |@@ -373,7 +378,7 @@ #### mock と mockM の使い分け -対象となる関数の**戻り値の型**に合わせて選択してください。+呼び出され方の検証が必要だと判断した後、対象となる関数の**戻り値の型**に合わせて選択してください。 * **`mock` (純粋な関数向け)**: * `String -> Int` のような**純粋な関数**をモックする場合に使用します。@@ -395,7 +400,7 @@ * 記録処理が戻り値のアクション(`IO`)自体に組み込まれているため、高度な並列テストや強力な最適化がかかる環境下でも、非常に高い予測可能性を提供します。 > [!TIP]-> 迷ったときは、**「ターゲットの関数が IO を返すなら `mockM`、そうでないなら `mock`」** と覚えておけば間違いありません。+> 呼び出され方の検証が必要だと判断した後は、**「ターゲットの関数が IO を返すなら `mockM`、そうでないなら `mock`」** と覚えておけば間違いありません。 #### 部分モック (Partial Mock): 本物の関数と混ぜて使う @@ -421,10 +426,12 @@ #### 派生とカスタムインスタンス (Derivation and Custom Instances) -`MockT` を使用する際、モック対象の副作用とは直接関係のない型クラスを扱わなければならないことがあります。Mockcat は、これらのケースを補助するためのマクロを提供しています。+`MockT` を使用する際、モック対象の副作用とは直接関係のない型クラスを扱わなければならないことがあります。 +Mockcat は、これらのケースを補助するためのマクロを提供しています。 ##### MTL インスタンス (`MonadReader`, `MonadError` 等)-`MockT` は、標準的な `mtl` の型クラス(`MonadReader`, `MonadError`, `MonadState`, `MonadWriter`)のインスタンスを標準で備えています。これらのインスタンスは、操作を自動的にベースモナドへリフト(持ち上げ)します。+`MockT` は、標準的な `mtl` の型クラス(`MonadReader`, `MonadError`, `MonadState`, `MonadWriter`)のインスタンスを標準で備えています。 +これらのインスタンスは、操作を自動的にベースモナドへリフト(持ち上げ)します。 ##### カスタム型クラスの派生 (`deriveMockInstances`) ベースモナドへリフトするだけでよいカスタムの "Capability" 型クラス(`MonadLogger`, `MonadConfig` 等)については、`deriveMockInstances` を使用できます。@@ -451,26 +458,38 @@ --- -#### 設計思想: Capability vs. Control+#### 逐次応答 -Mockcat は、型クラスの派生において **Capability (能力)** と **Control (制御)** を区別します。+case は Haskell のパターンマッチと同様に上から順に照合され、最初に一致したcase だけが選ばれます。 +各 case は独立した応答列を持ち、その case を選んだ呼び出しだけが列を進めます。 +末尾へ到達した後は最後の応答を返し続けます。 -* **Capability (注入/リフト)**: コンテキストやツールを提供する型クラス(例:`MonadReader`, `MonadLogger`)。- * **アプローチ**: `deriveMockInstances` や標準の `mtl` インスタンスを使用します。環境の一貫性を保つため、これらはベースモナドへリフトされるべきです。-* **Control (モック)**: 外部への副作用やビジネスロジックの境界を表す型クラス(例:`UserRepository`, `PaymentGateway`)。- * **アプローチ**: `makeMock` を使用します。テスト対象のロジックを隔離するため、これらは明示的にスタブ定義や検証が行われる必要があります。+つまり、入力による分岐を `onCase` で記述し、選ばれた case の呼び出しに伴う時間的な変化を `andThen` で記述します。 ----+```haskell+f <- mock do+ onCase $ "A" ~> 1+ `andThen` 2+ `andThen` 3+ onCase $ any @String ~> 9+ `andThen` 10+ `andThen` 11 +-- 呼び出し: A, B, A, C, A, B, A, C+-- 結果: 1, 9, 2, 10, 3, 11, 3, 11+``` +後ろにある重複caseには到達しません。 +同じ条件で値を順番に返す場合は、一つのcaseへ `andThen` で応答を追加してください。+ #### IO アクションを返す (Monadic Return) -`IO` を返す関数で、呼び出しごとに副作用(結果)を変えたい場合に使います。+`IO`を返す関数で呼び出しごとに副作用や結果を変える場合にも、`andThen` を利用できます。 ```haskell f <- mock do onCase $ "get" ~> pure @IO 1 -- 1回目- onCase $ "get" ~> pure @IO 2 -- 2回目+ `andThen` pure @IO 2 -- 2回目以降 ``` #### 名前付きモック@@ -489,7 +508,7 @@ ### 宣言的検証 DSL (`expects`) -`expects` ブロックでは、ビルダースタイルの構文を使って宣言的に期待値を記述できます。+`expects` ブロックでは、ビルダースタイルの構文を使って宣言的に期待値を記述できます。 `shouldBeCalled` と共通の語彙を使用しています。 #### 基本的な使い方@@ -595,8 +614,8 @@ ## 実践的な例 -Mockcat は特定のアーキテクチャを前提としません。-テスト基盤の中核として使うことも、既存のフレームワーク内で軽量なヘルパーとして使うこともできます。+Mockcat はプロダクションコードの設計を規定しません。 +純粋なコードには普通の値や関数を使い、小さく直接的なテストダブルが役立つ境界にだけ Mockcat を導入します。 以下は mockcat を使った実践的なテストスイートです: @@ -609,7 +628,7 @@ ## ヒントとトラブルシューティング ### `any` と `Prelude.any` の名前衝突-`Test.MockCat` をインポートすると、パラメータマッチャの `any` が `Prelude.any` と衝突することがあります。+`Test.MockCat` をインポートすると、パラメータマッチャの `any` が `Prelude.any` と衝突することがあります。 その場合は `Prelude` の `any` を隠すか、修飾名を使用してください。 ```haskell@@ -619,7 +638,7 @@ ``` ### `when` と `Control.Monad.when` の名前衝突-`Test.MockCat` は `when` (パラメータマッチャ) をエクスポートするため、`Control.Monad` の `when` (条件分岐) と衝突することがあります。+`Test.MockCat` は `when` (パラメータマッチャ) をエクスポートするため、`Control.Monad` の `when` (条件分岐) と衝突することがあります。 その場合は `Test.MockCat` からの `when` を隠すか、修飾名を使用してください。 ```haskell
README.md view
@@ -1,6 +1,6 @@ <div align="center"> <img src="https://raw.githubusercontent.com/pujoheadsoft/mockcat/main/logo.png" width="600px" alt="Mockcat Logo">- <h1>Declarative mocking with a single arrow <code>~></code></h1>+ <h1>Stub Haskell functions. Verify calls when needed.</h1> </div> <div align="center">@@ -13,8 +13,10 @@ </div> -**Mockcat** is a lightweight, declarative mocking library for Haskell.+**Mockcat** is a test double library for Haskell.+Its two core functions are `stub` and `mock`. +Defining a stub takes just one line: ```haskell -- Stub: Returns True when given "a" let f :: String -> Bool@@ -23,10 +25,11 @@ f "a" -- => True ``` -That's it. Write the argument on the left of `~>`, the return value on the right.+Write the argument on the left of `~>` and the return value on the right. No verification—just a pure function. -**To verify calls**, use `mock` with `expects`:+When you need to verify calls, use `mock`.+With `expects`, you can declare expectations before execution: ```haskell withMockIO $ do@@ -37,43 +40,43 @@ -- Verification runs when exiting the withMockIO scope ``` -> **When to use which:**-> - Just need a return value? Use `stub` (pure, no verification)-> - Need to verify calls? Use `mock` (always with `expects`)+> **Recommended usage:**+> - If an ordinary value or lambda is enough, use it directly.+> - Need fixed return values? Use `stub` (pure, no verification).+> - Need to verify arguments, call counts, or order? Use `mock`. --- ## Concepts & Terminology -Mockcat adopts a design where "verification happens at runtime, but 'conditions to be met' can be declared at definition time."+Mockcat separates stubbing return values from verifying calls. * **Stub**:- Exists solely to keep the test moving by returning values. It does not care "how it was called".- The `stub` function returns a completely pure function.+ Returns configured values for expected arguments.+ Use it when you do not need to verify call history. * **Mock**:- In addition to stubbing, it records and verifies "was it called as expected?".- The `mock` function returns a value while recording calls. Verification can be done at the end of the test, or declared as "it must be called this way" at definition time.+ Adds call history to a stub, allowing you to verify that it was called as expected.+ Expectations can be verified after execution or declared before execution. --- ## Why Mockcat? -There's no need to brace yourself when writing mocks in Haskell.--Mockcat is a mocking library that **"allows you to declaratively describe function behavior and intent without depending on specific architectures."**+Pure logic can be tested directly with values and functions, so it usually needs+neither stubs nor mocks. When you do need a test double, Mockcat provides a small,+straightforward way to define its behavior and verify calls when necessary. -"I can't test unless I introduce Typeclasses (MTL)."-"I have to define dedicated data types just for mocking."-(e.g., adding extra Typeclasses or Service Handle records just for testing)+Mockcat lets you **declaratively describe function behavior and calls without depending on a specific architecture.** -You are freed from such constraints. You can mock existing functions as they are, and start writing tests even when the design isn't fully solidified.+It works with ordinary functions, functions returning `IO`, functions passed as+arguments, Service Handle fields, and MTL/Capability typeclasses. -**Mockcat aims to let you write tests to explore design, rather than forcing you to fixate the design just for testing.**+**Mockcat follows your architecture—not the other way around.** ### Before / After -See how simple writing tests in Haskell can be.+Here is what the same test setup looks like with Mockcat. | | **Before: Handwritten...** 😫 | **After: Mockcat** 🐱✨ | | :--- | :--- | :--- |@@ -82,8 +85,8 @@ ### Key Features -* **Haskell Native DSL**: No need to memorize redundant data constructors or specialized notation. Write mocks naturally, just like function definitions (`arg ~> return`).-* **Architecture Agnostic**: Whether using MTL (Typeclasses), Service Handle (Records), or pure functions—Mockcat adapts to your design choice with minimal friction.+* **Haskell Native DSL**: No need to memorize redundant data constructors or specialized notation. Write test doubles naturally, just like function definitions (`arg ~> return`).+* **Architecture Agnostic**: Whether using MTL (Typeclasses), Service Handle (Records), or functions—Mockcat adapts to the production design you already have. * **Verify by "Condition", not just Value**: Works even if arguments lack `Eq` instances. You can verify based on "what properties it should satisfy" (Predicates) rather than just strict equality. * **Helpful Error Messages**: Shows "structural diffs" on failure, highlighting exactly what didn't match. ```text@@ -98,7 +101,7 @@ but got: 21 ^^ ```-* **Intent-Driven Types**: Types exist not to restrict you, but to naturally guide you in expressing your testing intent.+* **Intent-Driven Types**: Types help express testing intent without imposing a particular architecture. ---@@ -158,12 +161,12 @@ ## User Guide -Mockcat supports two verification styles depending on your testing needs and preferences.+Mockcat supports two verification styles, depending on when you want to state the expectations. -### 1. Declarative Verification (`withMock` / `expects`) - [Recommended]+### 1. Declarative Verification (`withMock` (`withMockIO`) / `expects`) -A style where you describe expectations at definition time. Verification runs automatically when exiting the scope.-Useful when you want "Definition" and "Verification" to be written close together.+Declare expectations when defining the mock. Verification runs automatically+when the scope exits. This keeps the mock definition and its expectations close together. ```haskell import Test.Hspec@@ -205,7 +208,7 @@ > [!IMPORTANT] > When using `expects` (declarative verification), you MUST wrap the mock definition in **parentheses `(...)`**.-> The `$` operator pattern used in previous versions (`mock $ ... expected ...`) will cause compilation errors due to precedence changes.+> The `$` operator pattern used in previous versions (`mock $ ... expects ...`) will cause compilation errors due to precedence changes. > > ❌ `mock $ any ~> True expects ...` > ✅ `mock (any ~> True) expects ...`@@ -223,7 +226,9 @@ ### 2. Mocking with Typeclass-Based Designs (`makeMock`) For designs that express dependencies via typeclasses (MTL style or Capability pattern),-you can bring them directly into tests. Generates mocks from typeclasses using Template Haskell.+Mockcat can generate mocks from those typeclasses using Template Haskell.+This option is for designs that already use typeclasses; you do not need to+introduce a typeclass just for testing. ```haskell {-# LANGUAGE TemplateHaskell #-}@@ -273,8 +278,9 @@ ### 3. Function Mocking and Post-Verification (`mock` / `shouldBeCalled`) -The most basic usage. Creates a function that returns values for specific arguments.-Combining it with Post-Verification (`shouldBeCalled`) makes it suitable for exploratory testing or prototyping.+Create a `mock`, exercise the code under test, and then verify the recorded calls+with `shouldBeCalled`. Use this style when verification reads more naturally+after execution. ```haskell import Test.Hspec@@ -323,7 +329,7 @@ ### 4. Flexible Verification (Matchers) Even if arguments don't have `Eq` instances, or you don't want to depend on specific values, you can verify based on **intent**—"what condition should be met".-Mockcat provides **matchers** to verify properties of functions, not just value equality.+Mockcat provides **matchers** for argument properties, not just exact value equality. #### Allow Any Value (`any`) @@ -337,8 +343,8 @@ #### Verify with Conditions (`when`) -You can verify using "conditions (predicates)" instead of arbitrary values.-Powerfully useful for types without `Eq` (like functions) or when checking partial matches.+You can use conditions (predicates) instead of exact values.+This is useful for types without `Eq` (such as functions), or when checking partial matches. ```haskell -- Return False only if the argument starts with "error"@@ -357,8 +363,8 @@ #### mock vs stub vs mockM -Choose the function based on the nature of your test target.-See the table below for details.+These functions differ in whether they record calls and where that recording+takes place. | Function | Verification (`shouldBeCalled`) | IO Dependency | Characteristics | | :--- | :---: | :---: | :--- |@@ -368,7 +374,7 @@ #### Choosing between `mock` and `mockM` -Choose the function according to the **return type** of the target function.+Choose between `mock` and `mockM` according to the **return type** of the target function. * **`mock` (For Pure Functions)**: * Use this when mocking **pure functions** like `String -> Int`.@@ -387,10 +393,10 @@ * **`mockM` (For IO/Monadic Functions)**: * Use this when mocking functions that return **`IO` or other `MonadIO` instances** (such as `ReaderT IO`), like `String -> IO Int`.- * Since the recording logic is built directly into the returned action (`IO`), it provides extremely high predictability even in highly concurrent tests or environments with heavy GHC optimizations.+ * Since recording is built directly into the returned action (`IO`), call counts remain predictable under concurrency and GHC optimizations. > [!TIP]-> When in doubt, remember: **"If the function returns IO, use `mockM`. Otherwise, use `mock`."**+> **If the function returns IO, use `mockM`. Otherwise, use `mock`.** #### Partial Mocking: Mixing with Real Functions @@ -446,26 +452,41 @@ --- -#### Design Philosophy: Capability vs. Control+#### Sequential Responses -Mockcat makes a distinction between **Capability** and **Control** when it comes to type class derivation.+Cases are matched from top to bottom, like Haskell pattern matching. Only the+first matching case is selected. Each case owns its response sequence, and only+calls selecting that case advance it. The final response is repeated after the+sequence is exhausted. -* **Capability (Inject/Lift)**: Type classes that provide context or tools (e.g., `MonadReader`, `MonadLogger`).- * **Approach**: Use `deriveMockInstances` or standard `mtl` instances. These should be lifted to the base monad to keep the environment consistent.-* **Control (Mock)**: Type classes that represent external side effects or business logic boundaries (e.g., `UserRepository`, `PaymentGateway`).- * **Approach**: Use `makeMock`. These must be explicitly stubbed or verified to ensure the test isolates the logic under test.+In short, `onCase` describes branching by input; `andThen` describes the sequence+of responses for repeated calls to that case. ----+```haskell+f <- mock do+ onCase $ "A" ~> 1+ `andThen` 2+ `andThen` 3+ onCase $ any @String ~> 9+ `andThen` 10+ `andThen` 11 +-- Calls: A, B, A, C, A, B, A, C+-- Results: 1, 9, 2, 10, 3, 11, 3, 11+``` +Later overlapping cases are unreachable. To return consecutive values for the+same condition, attach them to one case with `andThen`.+ #### Monadic Return (`IO a`) -Used when you want a function returning `IO` to have different side effects (results) for each call.+`andThen` also works with monadic return values when you want different effects+or results for consecutive calls. ```haskell f <- mock do onCase $ "get" ~> pure @IO 1 -- 1st call- onCase $ "get" ~> pure @IO 2 -- 2nd call+ `andThen` pure @IO 2 -- 2nd and later calls ``` #### Named Mocks@@ -590,8 +611,9 @@ ## Real-World Examples -Mockcat does not assume any specific architecture.-It can be used as the core of your testing foundation, or as a lightweight helper within an existing framework.+Mockcat does not dictate production architecture. Use ordinary values and+functions for pure code, and introduce Mockcat only at boundaries where a small,+direct test double is useful. Here are real-world test suites using mockcat:
mockcat.cabal view
@@ -5,10 +5,10 @@ -- see: https://github.com/sol/hpack name: mockcat-version: 1.4.1.1-synopsis: Declarative mocking with a single arrow `~>`.-description: Mockcat is a minimal, architecture-agnostic mocking library for Haskell.- It enables declarative verification and intent-driven matching, allowing you to define function behavior and expectations without specific architectural dependencies.+version: 1.5.0.0+synopsis: Stub Haskell functions and verify calls when needed.+description: Mockcat is a small, architecture-agnostic test double library for Haskell.+ It provides pure stubs for configured return values and mocks for verifying calls, without requiring a particular dependency injection style. . See README for full examples: <https://github.com/pujoheadsoft/mockcat#readme> category: Testing@@ -16,16 +16,17 @@ bug-reports: https://github.com/pujoheadsoft/mockcat/issues author: funnycat <pujoheadsoft@gmail.com> maintainer: funnycat <pujoheadsoft@gmail.com>-copyright: 2025 funnycat+copyright: 2025-2026 funnycat license: MIT license-file: LICENSE build-type: Simple tested-with: GHC == 9.2.8 , GHC == 9.4.8- , GHC == 9.6.3- , GHC == 9.8.2- , GHC == 9.10.1+ , GHC == 9.6.7+ , GHC == 9.8.4+ , GHC == 9.10.3+ , GHC == 9.12.2 extra-source-files: README.md README-ja.md
src/Test/MockCat.hs view
@@ -1,7 +1,7 @@ {-| Module : Test.MockCat-Description : Declarative mocking with a single arrow '~>'-Copyright : (c) Setup, 2025+Description : Stub Haskell functions and verify calls when needed+Copyright : (c) funnycat, 2025-2026 License : MIT Maintainer : kenji
src/Test/MockCat/Internal/Builder.hs view
@@ -27,6 +27,7 @@ , writeTVar ) import Data.Maybe+import Data.List (find, lookup) import Test.MockCat.Cons (Head(..), (:>)(..)) import Test.MockCat.Param import Prelude hiding (lookup)@@ -243,6 +244,7 @@ InvocationRecord { invocations = mempty , invocationCounts = []+ , caseInvocationCounts = [] } appendCalledParams :: TVar (InvocationRecord params) -> params -> IO ()@@ -389,14 +391,15 @@ params -> args -> InvocationStep args r-singleInvocationStep name params inputParams record@InvocationRecord {invocations, invocationCounts} = do+singleInvocationStep name params inputParams record@InvocationRecord {invocations, invocationCounts, caseInvocationCounts} = do let expected = projArgs params if expected `eqParams` inputParams then (InvocationRecord { invocations = invocations ++ [inputParams] , invocationCounts = invocationCounts- }, Right (returnValue params))+ , caseInvocationCounts = incrementCount 0 caseInvocationCounts+ }, Right (returnValueAt (lookupCount 0 caseInvocationCounts) params)) else (record, Left $ message name expected inputParams) casesInvocationStep ::@@ -405,31 +408,33 @@ InvocationList params -> args -> InvocationStep args r-casesInvocationStep name paramsList inputParams InvocationRecord {invocations, invocationCounts} = do+casesInvocationStep name paramsList inputParams InvocationRecord {invocations, invocationCounts, caseInvocationCounts} = do let newInvocations = invocations ++ [inputParams]- matchedParams = filter (\params -> projArgs params `eqParams` inputParams) paramsList+ matchedParam = find (\(_, params) -> projArgs params `eqParams` inputParams) (zip [0..] paramsList) expectedArgs = projArgs <$> paramsList- in case matchedParams of- [] ->- ( InvocationRecord {invocations = newInvocations, invocationCounts},+ in case matchedParam of+ Nothing ->+ ( InvocationRecord {invocations = newInvocations, invocationCounts, caseInvocationCounts}, Left (messageForMultiMock name expectedArgs inputParams) )- _ ->- let calledCount = fromMaybe 0 (lookupEqParams inputParams invocationCounts)- index = min calledCount (length matchedParams - 1)- nextCounter = incrementCountEqParams inputParams invocationCounts+ Just (caseIndex, selected) ->+ let calledCount = lookupCount caseIndex caseInvocationCounts nextRecord = InvocationRecord- { invocations = newInvocations,- invocationCounts = nextCounter+ { invocations = newInvocations+ , invocationCounts = invocationCounts+ , caseInvocationCounts = incrementCount caseIndex caseInvocationCounts }- in case safeIndex matchedParams index of- Nothing ->- ( nextRecord,- Left (messageForMultiMock name expectedArgs inputParams)- )- Just selected ->- (nextRecord, Right (returnValue selected))+ in (nextRecord, Right (returnValueAt calledCount selected))++lookupCount :: Int -> [(Int, Int)] -> Int+lookupCount key = fromMaybe 0 . lookup key++incrementCount :: Int -> [(Int, Int)] -> [(Int, Int)]+incrementCount key [] = [(key, 1)]+incrementCount key ((existingKey, count):counts)+ | key == existingKey = (existingKey, count + 1) : counts+ | otherwise = (existingKey, count) : incrementCount key counts lookupEqParams :: EqParams k => k -> [(k, v)] -> Maybe v lookupEqParams _ [] = Nothing
src/Test/MockCat/Internal/Types.hs view
@@ -50,7 +50,8 @@ data InvocationRecord params = InvocationRecord { invocations :: InvocationList params,- invocationCounts :: InvocationCounts params+ invocationCounts :: InvocationCounts params,+ caseInvocationCounts :: [(Int, Int)] } deriving (Eq, Show)
src/Test/MockCat/Mock.hs view
@@ -405,7 +405,11 @@ stub :: CreateStubFn a => a stub = stubImpl -{- | Register a stub case within a 'Cases' builder. -}+{- | Register a stub case within a 'Cases' builder.++Cases are tested in declaration order and the first matching case is selected.+Use 'andThen' to attach consecutive responses to one case.+-} onCase :: a -> Cases a () onCase a = Cases $ do st <- get@@ -427,5 +431,3 @@ instance LiftFunTo restIO restM m => LiftFunTo (a -> restIO) (a -> restM) m where liftFunTo proxy f a = liftFunTo proxy (f a)--
src/Test/MockCat/Param.hs view
@@ -20,6 +20,8 @@ value, param, ConsGen(..),+ AndThen(..),+ ResponseOf, MockSpec(..), -- * Matchers when,@@ -37,6 +39,7 @@ ProjectionReturn, projReturn, returnValue,+ returnValueAt, ) where@@ -48,8 +51,11 @@ import Data.Typeable (Typeable, typeOf) import Foreign.Ptr (Ptr, ptrToIntPtr, castPtr, IntPtr) import qualified Data.Text as T (Text)+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NonEmpty -infixr 1 ~>+infixr 2 ~>+infixl 1 `andThen` -- | MockSpec wraps stub parameters with optional expectations. -- The 'exps' type parameter is () when no expectations are set,@@ -68,6 +74,8 @@ ExpectCondition :: (v -> Bool) -> String -> Param v -- | A parameter that wraps a value without Eq or Show constraints. ValueWrapper :: v -> String -> Param v+ -- | An ordered, non-empty sequence of return values.+ ResponseSequence :: NonEmpty (Param v) -> Param v -- | Class for wrapping raw values into Param for results.@@ -117,15 +125,20 @@ ExpectCondition _ "any" == ExpectCondition _ _ = True ExpectCondition _ _ == ExpectCondition _ "any" = True ExpectCondition _ l1 == ExpectCondition _ l2 = l1 == l2+ ResponseSequence a == ResponseSequence b = a == b+ ResponseSequence _ == _ = False+ _ == ResponseSequence _ = False instance Show (Param v) where show (ExpectValue _ l) = l show (ExpectCondition _ l) = l show (ValueWrapper _ l) = l+ show (ResponseSequence values) = show (NonEmpty.toList values) value :: Param v -> v value (ExpectValue a _) = a value (ValueWrapper a _) = a+value (ResponseSequence values) = value (NonEmpty.head values) value _ = error "not implemented" -- | Create a Param from a value. Requires Eq and Show.@@ -231,6 +244,38 @@ instance (ToParamParam a, ToParamResult b) => ConsGen a b where a ~> b = toParamParam a :> toParamResult b +-- | The return value type at the end of a mock parameter chain.+type family ResponseOf params where+ ResponseOf (Param a :> Param r) = r+ ResponseOf (Param a :> rest) = ResponseOf rest++-- | Append a response to a mock case.+--+-- @+-- onCase $ "A" ~> 1 `andThen` 2 `andThen` 3+-- @+--+-- Responses are selected in order. After the final response, the final+-- value is returned for all subsequent calls that select the same case.+class AndThen params where+ andThen :: params -> ResponseOf params -> params++instance {-# OVERLAPPING #-}+ (ToParamResult r, Normalize r ~ Param r) =>+ AndThen (Param a :> Param r) where+ (arg :> result) `andThen` next =+ arg :> appendResponse result (toParamResult next)++instance {-# OVERLAPPABLE #-}+ (AndThen rest, ResponseOf (Param a :> rest) ~ ResponseOf rest) =>+ AndThen (Param a :> rest) where+ (arg :> rest) `andThen` next = arg :> (rest `andThen` next)++appendResponse :: Param a -> Param a -> Param a+appendResponse (ResponseSequence responses) next =+ ResponseSequence (responses <> (next :| []))+appendResponse first next = ResponseSequence (first :| [next])+ -- | Make a parameter to which any value is expected to apply. -- Use with type application to specify the type: @any \@String@ --@@ -322,6 +367,19 @@ returnValue :: (ProjectionReturn params, ReturnOf params ~ Param r) => params -> r returnValue = value . projReturn++-- | Return the response at the given zero-based position. Ordinary return+-- values behave as a one-element sequence. Sequential responses keep+-- returning their final value after the sequence is exhausted.+returnValueAt :: (ProjectionReturn params, ReturnOf params ~ Param r) => Int -> params -> r+returnValueAt index = valueAt index . projReturn++valueAt :: Int -> Param r -> r+valueAt index (ResponseSequence responses) =+ let responseList = NonEmpty.toList responses+ selectedIndex = min (max 0 index) (length responseList - 1)+ in value (responseList !! selectedIndex)+valueAt _ result = value result -- | Get the pointer address of a value (used for both comparison and display) getPtrAddr :: forall a. a -> IntPtr
test/Property/AdditionalProps.hs view
@@ -76,7 +76,11 @@ prop_multicase_progression = forAll genSeq $ \(arg, rs, extra) -> monadicIO $ do let totalCalls = length rs + extra run $ withMock $ do- f <- mock (cases [ param arg ~> r | r <- rs ])+ let responseCase = case rs of+ [] -> error "genSeq produced an empty response sequence"+ firstResponse : remainingResponses ->+ foldl andThen (param arg ~> firstResponse) remainingResponses+ f <- mock (cases [responseCase]) `expects` called (times totalCalls) -- Run loop
test/Support/ParamSpec.hs view
@@ -78,3 +78,4 @@ fromParam (ExpectValue v _) = PSExact v fromParam (ExpectCondition f l) = PSPredicate f l fromParam (ValueWrapper _ _) = PSPredicate (const True) "any"+fromParam (ResponseSequence _) = error "ResponseSequence cannot be used as an argument matcher"
test/Test/MockCat/ExampleSpec.hs view
@@ -156,6 +156,13 @@ f <- mock $ "a" ~> (11 :: Int) ~> pure @IO False f "a" (11 :: Int) `shouldReturn` False + it "mock returns sequential monadic stubs (IO)" do+ f <- mock $ "a" ~> pure @IO (1 :: Int)+ `andThen` pure @IO 2+ f "a" `shouldReturn` 1+ f "a" `shouldReturn` 2+ f "a" `shouldReturn` 2+ it "mock returns monadic stub (MaybeT)" do mm <- runMaybeT do -- create a mock inside MaybeT@@ -204,7 +211,7 @@ it "Return different values for the same argument" do f <- mock $ do onCase $ "arg" ~> "x"- onCase $ "arg" ~> "y"+ `andThen` "y" -- Do not allow optimization to remove duplicates. v1 <- evaluate $ f "arg"
test/Test/MockCat/Internal/MockRegistrySpec.hs view
@@ -13,7 +13,7 @@ describe "MockRegistry" do it "register and lookup" do let f = (+ 1) :: Int -> Int- ref <- newTVarIO InvocationRecord { invocations = [] :: [Int], invocationCounts = [] }+ ref <- newTVarIO InvocationRecord { invocations = [] :: [Int], invocationCounts = [], caseInvocationCounts = [] } attachVerifierToFn f (Just "name", InvocationRecorder ref ParametricFunction) results <- lookupVerifierForFn f case results of@@ -22,8 +22,7 @@ case (fromDynamic dyn :: Maybe (InvocationRecorder Int)) of Just (InvocationRecorder vref _) -> do r <- readTVarIO vref- r `shouldBe` InvocationRecord { invocations = [] :: [Int], invocationCounts = [] }+ r `shouldBe` InvocationRecord { invocations = [] :: [Int], invocationCounts = [], caseInvocationCounts = [] } Nothing -> expectationFailure "payload dynamic mismatch" _ -> expectationFailure "lookupStubFn returned unexpected number of results"-
test/Test/MockCat/MockSpec.hs view
@@ -145,9 +145,9 @@ it "arity = 1" do f <- mock $ do onCase $ "a" ~> True+ `andThen` False onCase $ "b" ~> False- onCase $ "a" ~> False- onCase $ "b" ~> True+ `andThen` True v1 <- evaluate $ f "a" v2 <- evaluate $ f "a"@@ -161,9 +161,9 @@ it "arity = 2" do f <- mock $ do onCase $ "a" ~> "b" ~> (0 :: Int)+ `andThen` 2 onCase $ "a" ~> "c" ~> (1 :: Int)- onCase $ "a" ~> "b" ~> (2 :: Int)- onCase $ "a" ~> "c" ~> (3 :: Int)+ `andThen` 3 v1 <- evaluate $ f "a" "b" v2 <- evaluate $ f "a" "b"@@ -175,6 +175,41 @@ v3 `shouldBe` (1 :: Int) v4 `shouldBe` (3 :: Int) v5 `shouldBe` (2 :: Int)++ it "uses the first matching case and advances only that case" do+ f <- mock $ do+ onCase $ "A" ~> (1 :: Int)+ `andThen` 2+ `andThen` 3+ onCase $ any @String ~> (9 :: Int)+ `andThen` 10+ `andThen` 11++ values <- mapM (evaluate . f) ["A", "B", "A", "C", "A", "B", "A", "C"]+ values `shouldBe` [1, 9, 2, 10, 3, 11, 3, 11]++ it "uses the first case when conditions overlap" do+ f <- mock $ do+ onCase $ "A" ~> (1 :: Int)+ onCase $ "A" ~> (2 :: Int)++ values <- mapM (evaluate . f) ["A", "A"]+ values `shouldBe` [1, 1]++ it "shares an any case response sequence across argument values" do+ f <- mock $ any @String ~> (1 :: Int)+ `andThen` 2++ values <- mapM (evaluate . f) ["A", "B", "A", "B"]+ values `shouldBe` [1, 2, 2, 2]++ it "does not require Eq for sequential matcher arguments" do+ f <- mock $ any @NoEq ~> (1 :: Int)+ `andThen` 2++ first <- evaluate $ f (NoEq "A")+ second <- evaluate $ f (NoEq "B")+ [first, second] `shouldBe` [1, 2] describe "constant" do it "mock" do
test/Test/MockCat/Readme/MatcherSpec.hs view
@@ -22,6 +22,7 @@ f <- mock $ do onCase $ when (\s -> "error" `isPrefixOf` s) "start with error" ~> False onCase $ any ~> True+ f "error again" `shouldBe` False f "error message" `shouldBe` False f "success" `shouldBe` True
test/Test/MockCat/ShouldBeCalledMockMSpec.hs view
@@ -21,6 +21,14 @@ result `shouldBe` True f `shouldBeCalled` "a" + it "returns consecutive responses for the selected case" do+ f <- mockM $ any @String ~> (1 :: Int)+ `andThen` 2+ first <- f "a"+ second <- f "b"+ third <- f "a"+ [first, second, third] `shouldBe` [1, 2, 2]+ it "fails when called with unexpected argument" do f <- mockM $ "a" ~> True void $ f "a"@@ -124,4 +132,3 @@ void $ f "lifted" liftIO $ f `shouldBeCalled` (times 2 `withArgs` "lifted")) ()-