mockcat 1.3.3.0 → 1.4.0.0
raw patch · 34 files changed
+835/−707 lines, 34 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Test.MockCat.AssociationList: (!?) :: Eq k => AssociationList k a -> k -> Maybe a
- Test.MockCat.AssociationList: empty :: AssociationList k a
- Test.MockCat.AssociationList: insert :: Eq k => k -> a -> AssociationList k a -> AssociationList k a
- Test.MockCat.AssociationList: lookup :: Eq k => k -> AssociationList k a -> Maybe a
- Test.MockCat.AssociationList: member :: Eq k => k -> AssociationList k a -> Bool
- Test.MockCat.AssociationList: type AssociationList k a = [(k, a)]
- Test.MockCat.AssociationList: update :: Eq k => (a -> a) -> k -> AssociationList k a -> AssociationList k a
- Test.MockCat.Internal.Builder: incrementCount :: Eq k => k -> InvocationCounts k -> InvocationCounts k
- Test.MockCat.Internal.Builder: instance Test.MockCat.Internal.Builder.MockBuilder (Test.MockCat.Param.Param r) r ()
- Test.MockCat.Internal.MockRegistry: markUnitUsed :: UnitMeta -> IO ()
- Test.MockCat.Internal.Registry.Core: clearOverlay :: IO ()
- Test.MockCat.Internal.Registry.Core: createOverlay :: IO Overlay
- Test.MockCat.Internal.Registry.Core: installOverlay :: Overlay -> IO ()
- Test.MockCat.Internal.Registry.Core: markUnitUsed :: UnitMeta -> IO ()
+ Test.MockCat.Internal.Types: type AssociationList k a = [(k, a)]
- Test.MockCat.Internal.Builder: incrementInvocationCount :: Eq params => TVar (InvocationRecord params) -> params -> IO ()
+ Test.MockCat.Internal.Builder: incrementInvocationCount :: EqParams params => TVar (InvocationRecord params) -> params -> IO ()
- Test.MockCat.Internal.Builder: readInvocationCount :: Eq params => TVar (InvocationRecord params) -> params -> IO Int
+ Test.MockCat.Internal.Builder: readInvocationCount :: EqParams params => TVar (InvocationRecord params) -> params -> IO Int
Files
- CHANGELOG.md +13/−0
- README-ja.md +145/−109
- README.md +132/−102
- mockcat.cabal +11/−5
- src/Test/MockCat/AssociationList.hs +0/−34
- src/Test/MockCat/Internal/Builder.hs +5/−25
- src/Test/MockCat/Internal/MockRegistry.hs +2/−5
- src/Test/MockCat/Internal/Registry/Core.hs +4/−46
- src/Test/MockCat/Internal/Types.hs +2/−1
- src/Test/MockCat/MockT.hs +1/−5
- src/Test/MockCat/Verify.hs +10/−19
- test/Property/AdditionalProps.hs +69/−42
- test/Property/ConcurrentCountProp.hs +8/−1
- test/Property/LazyEvalProp.hs +10/−1
- test/Property/OrderProps.hs +39/−18
- test/Property/ReinforcementProps.hs +45/−24
- test/Property/ScriptProps.hs +16/−6
- test/ReadmeVerifySpec.hs +0/−85
- test/Spec.hs +38/−36
- test/Test/MockCat/AssociationListSpec.hs +0/−48
- test/Test/MockCat/HPCFallbackSpec.hs +0/−59
- test/Test/MockCat/HpcSpec.hs +48/−0
- test/Test/MockCat/Internal/MockRegistrySpec.hs +2/−3
- test/Test/MockCat/PartialMockSpec.hs +11/−22
- test/Test/MockCat/Readme/MakeAutoLiftMockSpec.hs +37/−0
- test/Test/MockCat/Readme/MakeMockSpec.hs +38/−0
- test/Test/MockCat/Readme/MatcherSpec.hs +28/−0
- test/Test/MockCat/Readme/QuickStartDemoSpec.hs +18/−0
- test/Test/MockCat/Readme/ReadmeSpec.hs +24/−0
- test/Test/MockCat/Readme/ShouldBeCalledSpec.hs +16/−0
- test/Test/MockCat/Readme/WithMockIOSpec.hs +15/−0
- test/Test/MockCat/Readme/WithMockSpec.hs +19/−0
- test/Test/MockCat/TestHelper.hs +21/−0
- test/Test/MockCat/WithMockIOSpec.hs +8/−11
CHANGELOG.md view
@@ -6,6 +6,19 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## [1.4.0.0] - 2026-01-10+### Changed+- **Breaking Change**: Removed the verify fallback mechanism that searched thread history when `StableName` lookup failed.+ - **Impact**: Strict verification is now enforced. Tests using `shouldBeCalled` or related matchers will now deterministically fail if the mock function's identity cannot be strictly resolved (e.g., in some HPC/Coverage environments without proper handling), rather than attempting to guess the intent.+ - **Motivation**: To ensure absolute confidence in test results and support reliable HPC coverage analysis.++### Improved+- **HPC Code Coverage Support**: The library and test suite are now fully compatible with `stack test --coverage`.+ - Verification logic has been hardened to handle HPC instrumentation.+ - The test suite now automatically detects HPC mode and skips only those tests that strictly depend on `StableName` identity (which is inherently unstable under HPC), ensuring the rest of the suite runs safely.+- **Test Suite Modernization**: Refactored Property-based tests and `WithMockIO` specs to use the `expects` pattern (definition-time expectation). This makes them robust enough to strictly verify behavior even in HPC environments.+- **Granularity**: Improved module organization in `Spec.hs` for better maintainability.+ ## [1.3.3.0] - 2026-01-04 ### Added - **Type Families Support**: `deriveMockInstances` now supports type classes containing Associated Type Families.
README-ja.md view
@@ -14,14 +14,18 @@ </div> **Mockcat** は、Haskell のための直感的で宣言的なモックライブラリです。-専用の演算子 **Mock Arrow (`~>`)** を使うことで、関数定義と同じような感覚で、引数と振る舞いをモックとして記述できます。+2つの検証スタイルをサポートしています。 +* **事前宣言による検証 (`expects`)**: 【推奨】 モック定義時に期待される振る舞いを宣言します。+* **事後検証 (`shouldBeCalled`)**: テスト実行後に呼び出しを検証します。+ ```haskell--- 定義 (Define)+-- 定義と同時に検証内容を宣言 ("input" で1回呼ばれることを期待) f <- mock ("input" ~> "output")+ `expects` called once --- 検証 (Verify)-f `shouldBeCalled` "input"+-- 実行+f "input" ``` ---@@ -61,7 +65,7 @@ | | **Before: 手書き...** 😫 | **After: Mockcat** 🐱✨ | | :--- | :--- | :--- | | **定義 (Stub)**<br />「この引数には<br />この値を返したい」 | <pre>f :: String -> IO String<br />f arg = case arg of<br /> "a" -> pure "b"<br /> _ -> error "unexpected"</pre><br />_単純な分岐を書くだけでも行数を消費します。_ | <pre>-- 検証不要なら stub (純粋)<br />let f = stub ("a" ~> "b")</pre><br />_完全な純粋関数として振る舞います。_ |-| **検証 (Verify)**<br />「正しく呼ばれたか<br />テストしたい」 | <pre>-- 記録の仕組みから作る必要がある<br />ref <- newIORef []<br />let f arg = do<br /> modifyIORef ref (arg:)<br /> ...<br /><br />-- 検証ロジック<br />calls <- readIORef ref<br />calls `shouldBe` ["a"]</pre><br />_※ これはよくある一例です。実際にはさらに補助コードが増えがちです。_ | <pre>-- 検証したいなら mock (内部で記録)<br />f <- mock ("a" ~> "b")<br /><br />-- 検証したい内容を書くだけ<br />f `shouldBeCalled` "a"</pre><br />_記録は自動。<br />「何を検証するか」という本質に集中できます。_ |+| **検証 (Verify)**<br />「正しく呼ばれたか<br />テストしたい」 | <pre>-- 記録の仕組みから作る必要がある<br />ref <- newIORef []<br />let f arg = do<br /> modifyIORef ref (arg:)<br /> ...<br /><br />-- 検証ロジック<br />calls <- readIORef ref<br />calls `shouldBe` ["a"]</pre><br />_※ これはよくある一例です。実際にはさらに補助コードが増えがちです。_ | <pre>withMock $ do<br /> -- 定義と同時に期待値を宣言<br /> f <- mock ("a" ~> "b")<br /> `expects` called once<br /><br /> -- 実行するだけ (自動検証)</pre><br />_記録は自動。<br />「何を検証するか」という本質に集中できます。_ | ### 主な特徴 @@ -104,73 +108,92 @@ mockcat ``` -### 最初のテスト (`Main.hs` / `Spec.hs`)-+### 最初のテスト ```haskell-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE TypeApplications #-} import Test.Hspec import Test.MockCat -main :: IO ()-main = hspec spec- spec :: Spec spec = do- it "Quick Start Demo" do- -- 1. モックを作成 ("Hello" を受け取ったら 42 を返す)- f <- mock ("Hello" ~> (42 :: Int))+ it "Quick Start Demo" $ do+ withMockIO $ do+ -- 1. モックを作成 ("Hello" を受け取ったら 42 を返す)+ -- 同時に「1回呼ばれるはずだ」と期待を宣言+ f <- mock ("Hello" ~> (42 :: Int))+ `expects` called once - -- 2. 関数として使う- let result = f "Hello"- result `shouldBe` 42+ -- 2. f "Hello" を呼び出し、42 を得る+ f "Hello" `shouldBe` 42 - -- 3. 呼び出されたことを検証- f `shouldBeCalled` "Hello"+ -- 3. スコープを抜ける際、宣言した期待に対する検証が自動で行われる ``` ---- -### At a Glance: Matchers-| Matcher | Description | Example |-| :--- | :--- | :--- |-| **`any`** | どんな値でも許可 | `f <- mock (any ~> True)` |-| **`when`** | 条件(述語)で検証 | `f <- mock (when (> 5) "gt 5" ~> True)` |-| **`"val"`** | 値の一致 (Eq) | `f <- mock ("val" ~> True)` |-| **`inOrder`** | 順序検証 | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |-| **`inPartial`**| 部分順序 | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` | ----- ## 使い方ガイド (User Guide) -Mockcat は、テストの目的や好みに応じて 2 つの検証スタイルをサポートしています。--1. **事後検証スタイル (Spy)**:- とりあえずモックで振る舞いを定義して実行し、後から `shouldBeCalled` で検証するスタイル。探索的なテストや、セットアップを簡単に済ませたい場合に適しています。(以下のセクション 1, 2 で主に使用)-2. **事前期待スタイル (Declarative/Expectation)**:- 定義と同時に「こう呼ばれるべき」という期待を記述するスタイル。厳密なインタラクションのテストに適しています。(以下のセクション 3 で解説)+Mockcat は、テストの目的や環境に応じて 2 つの検証スタイルをサポートしています。 -### 1. 関数のモック (`mock`) - [基本]+### 1. 宣言的な検証 (`withMock` (`withMockIO`) / `expects`) - [推奨] -最も基本的な使い方です。特定の引数に対して値を返す関数を作ります。+定義と同時に期待値を記述するスタイルです。スコープを抜ける時に自動的に検証が走ります。+「定義」と「検証」を近くに書きたい場合に便利です。 ```haskell--- "a" -> "b" -> True を返す関数-f <- mock ("a" ~> "b" ~> True)+import Test.Hspec+import Test.MockCat+import Control.Monad.IO.Class (MonadIO(liftIO))++spec :: Spec+spec = do+ it "User Guide (withMock)" $ do+ withMock $ do+ -- "Hello" に対して True を返すモックを定義+ f <- mock ("Hello" ~> True)+ `expects` called once++ -- 実行+ let result = f "Hello"++ liftIO $ result `shouldBe` True ``` -**柔軟なマッチング**:-具体的な値だけでなく、条件(述語)を指定することもできます。+#### `withMockIO`: IO テストの簡略化+`withMockIO` は `withMock` を IO に特化させたバージョンです。`liftIO` を使わずにモックコンテキスト内で直接 IO アクションを実行できます。 ```haskell--- 任意の文字列 (param any)-f <- mock (any ~> True)+import Test.Hspec+import Test.MockCat --- 条件式 (when)-f <- mock (when (> 5) "> 5" ~> True)+spec :: Spec+spec = do+ it "User Guide (withMockIO)" $ do+ withMockIO $ do+ f <- mock ("Hello" ~> True)+ `expects` called once++ let result = f "Hello"++ result `shouldBe` True ``` +> [!IMPORTANT]+> `expects`(宣言的検証)を使用する場合、モック定義部分は必ず **括弧 `(...)`** で囲んでください。+> 以前のバージョンで使用できた `$` 演算子 (`mock $ ... expected ...`) は、優先順位の関係でコンパイルエラーになります。+>+> ❌ `mock $ any ~> True expects ...`+> ✅ `mock (any ~> True) expects ...`++> [!NOTE]+> `runMockT` ブロックの中でも、同様に `expects` を使った宣言的検証が可能です。+> 生成された型クラスのモック関数(`_xxx`)に対してもそのまま使用できます。+>+> ```haskell+> runMockT do+> _readFile "config.txt" ~> pure "value"+> `expects` called once+> ```+ ### 2. 型クラスのモック (`makeMock`) 既存の型クラスをそのままテストに持ち込みたい場合に使います。Template Haskell を使って、既存の型クラスからモックを自動生成します。@@ -221,48 +244,54 @@ result `shouldBe` () ``` -### 3. 宣言的な検証 (`withMock` / `expects`)+### 3. 関数のモックと事後検証 (`mock` / `shouldBeCalled`) -定義と同時に期待値を記述するスタイルです。スコープを抜ける時に自動的に検証が走ります。-「定義」と「検証」を近くに書きたい場合に便利です。+`withMock` (`withMockIO`) を使用せずに、特定の引数に対して値を返す関数を作ることもできます。+その場合は、事後検証 (`shouldBeCalled`) を組み合わせることになります。 ```haskell-withMock $ do- -- 定義と同時に期待値(expects)を書く- f <- mock (any ~> True)- `expects` do- called once `with` "arg"+import Test.Hspec+import Test.MockCat - -- 実行- f "arg"+spec :: Spec+spec = do+ it "Function Mocking" $ do+ -- "Hello" に対して 1 を返す関数を定義 (expects は書かない)+ f <- mock ("Hello" ~> True)+ + -- 実行+ f "Hello" `shouldBe` True -#### `withMockIO`: IO テストの簡略化-`withMockIO` は `withMock` を IO に特化させたバージョンです。`liftIO` を使わずにモックコンテキスト内で直接 IO アクションを実行できます。+ -- 事後検証 (shouldBeCalled)+ f `shouldBeCalled` "Hello"+``` +> [!WARNING]+> **HPC (コードカバレッジ) 環境での制限**+> `stack test --coverage` 等を使用する場合、`shouldBeCalled` は使用しないでください。+> GHC のカバレッジ計測機能が関数をラップするため、関数の同一性が失われ、検証に失敗します。+> カバレッジ計測が必要な場合は、**`expects`** スタイル (Section 1) を使用してください。++**柔軟なマッチング**:+具体的な値だけでなく、条件(述語)を指定することもできます。+ ```haskell-it "IO test" $ withMockIO do- f <- mock (any ~> pure "result")- res <- someIOCall f- res `shouldBe` "result"-```-```+{-# LANGUAGE TypeApplications #-}+import Test.Hspec+import Test.MockCat+import Prelude hiding (any) -> [!IMPORTANT]-> `expects`(宣言的検証)を使用する場合、モック定義部分は必ず **括弧 `(...)`** で囲んでください。-> 以前のバージョンで使用できた `$` 演算子 (`mock $ ... expected ...`) は、優先順位の関係でコンパイルエラーになります。->-> ❌ `mock $ any ~> True expects ...`-> ✅ `mock (any ~> True) expects ...`+spec :: Spec+spec = do+ it "Matcher Examples" $ do+ -- 任意の文字列 (param any)+ f <- mock (any @String ~> True)+ f "foo" `shouldBe` True -> [!NOTE]-> `runMockT` ブロックの中でも、同様に `expects` を使った宣言的検証が可能です。-> 生成された型クラスのモック関数(`_xxx`)に対してもそのまま使用できます。->-> ```haskell-> runMockT do-> _readFile "config.txt" ~> pure "value"-> `expects` called once-> ```+ -- 条件式 (when)+ g <- mock (when (> (5 :: Int)) "> 5" ~> True)+ g 6 `shouldBe` True+``` ### 4. 柔軟な検証(マッチャー) @@ -273,7 +302,7 @@ ```haskell -- どんな引数で呼ばれても True を返す-f <- mock (any ~> True)+f <- mock (any @String ~> True) -- 何でもいいから呼ばれたことを検証 f `shouldBeCalled` any@@ -285,16 +314,21 @@ `Eq` を持たない型(関数など)や、部分的な一致を確認したい場合に強力です。 ```haskell--- 引数が "error" で始まる場合のみ False を返す-f <- mock do- onCase $ when (\s -> "error" `isPrefixOf` s) "start with error" ~> False- onCase $ any ~> True+it "When Example" $ do+ -- 引数が "error" で始まる場合のみ False を返す+ f <- mock $ do+ onCase $ when (\s -> "error" `isPrefixOf` s) "start with error" ~> False+ onCase $ any ~> True++ f "error message" `shouldBe` False+ f "success" `shouldBe` True+ f "other" `shouldBe` True ``` ラベル(エラー時に表示される説明)が不要な場合は、`when_` を使用することもできます。 ```haskell-f <- mock (when_ (> 5) ~> True)+f <- mock (when_ (> (5 :: Int)) ~> True) ``` ### 5. 高度な機能 - [応用]@@ -400,28 +434,6 @@ ※ このセクションは、困ったときの辞書として使ってください。 -### 検証マッチャ一覧 (`shouldBeCalled`)--| マッチャ | 説明 | 例 |-| :--- | :--- | :--- |-| `x` (値そのもの) | その値で呼ばれたか | ``f `shouldBeCalled` (10 :: Int)`` |-| `times n` | 回数指定 | ``f `shouldBeCalled` (times 3 `with` "arg")`` |-| `once` | 1回だけ | ``f `shouldBeCalled` (once `with` "arg")`` |-| `never` | 呼ばれていない | ``f `shouldBeCalled` never`` |-| `atLeast n` | n回以上 | ``f `shouldBeCalled` atLeast 2`` |-| `atMost n` | n回以下 | ``f `shouldBeCalled` atMost 5`` |-| `anything` | 引数は何でも良い(回数不問) | ``f `shouldBeCalled` anything`` |-| `inOrderWith [...]` | 厳密な順序 | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |-| `inPartialOrderWith [...]` | 部分的順序(間飛びOK) | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` |--### パラメータマッチャ一覧(引数定義)--| マッチャ | 説明 | 例 |-| :--- | :--- | :--- |-| `any` | 任意の値 | `any ~> True` |-| `when pred label` | 条件式 | `when (>0) "positive" ~> True` |-| `when_ pred` | ラベルなし | `when_ (>0) ~> True` |- ### 宣言的検証 DSL (`expects`) `expects` ブロックでは、ビルダースタイルの構文を使って宣言的に期待値を記述できます。@@ -456,6 +468,28 @@ | **`with matcher`** | マッチャを使って引数を検証します。 | `called `with` when (>5) "gt 5"` | | **`inOrder`** | 呼び出し順序を検証 (リスト内で使用) | (順序検証の項を参照) | +### 検証マッチャ一覧 (`shouldBeCalled`)++| マッチャ | 説明 | 例 |+| :--- | :--- | :--- |+| `x` (値そのもの) | その値で呼ばれたか | ``f `shouldBeCalled` (10 :: Int)`` |+| `times n` | 回数指定 | ``f `shouldBeCalled` (times 3 `with` "arg")`` |+| `once` | 1回だけ | ``f `shouldBeCalled` (once `with` "arg")`` |+| `never` | 呼ばれていない | ``f `shouldBeCalled` never`` |+| `atLeast n` | n回以上 | ``f `shouldBeCalled` atLeast 2`` |+| `atMost n` | n回以下 | ``f `shouldBeCalled` atMost 5`` |+| `anything` | 引数は何でも良い(回数不問) | ``f `shouldBeCalled` anything`` |+| `inOrderWith [...]` | 厳密な順序 | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |+| `inPartialOrderWith [...]` | 部分的順序(間飛びOK) | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` |++### パラメータマッチャ一覧(引数定義)++| マッチャ | 説明 | 例 |+| :--- | :--- | :--- |+| `any` | 任意の値 | `any ~> True` |+| `when pred label` | 条件式 | `when (>0) "positive" ~> True` |+| `when_ pred` | ラベルなし | `when_ (>0) ~> True` |+ ### よくある質問 (FAQ) <details>@@ -470,7 +504,9 @@ <details> <summary><strong>Q. コードカバレッジ (HPC) を有効にしてテストを実行できますか?</strong></summary>-A. はい (v1.1.0.0 以降)。Mockcat は HPC によって生じる 不安定さを内部で吸収するため、`stack test --coverage` を問題なく実行できます。+A. はい (v1.1.0.0 以降)。Mockcat の `expects` スタイルは、HPC による関数のラップの影響を受けない設計になっているため、HPC下でも安全に動作します。+ただし、前述の理由により **`expects`** スタイル (または `withMock`) の使用を推奨します。+`shouldBeCalled` スタイルは、HPC の仕組み上、モックの同一性を特定できないため使用できません。 </details> <details>
README.md view
@@ -14,14 +14,18 @@ </div> **Mockcat** is a lightweight, declarative mocking library for Haskell. -By using the dedicated **Mock Arrow (`~>`)** operator, you can describe mock behavior with the same ease as defining standard functions.+It supports two verification styles: +* **Declarative Verification (`expects`)**: **[Recommended]** Declaratively state the expected behavior at mock definition time.+* **Post-hoc Verification (`shouldBeCalled`)**: Verify call history after test execution.+ ```haskell--- Define+-- Define and Expect at the same time ("input" should be called exactly once) f <- mock ("input" ~> "output")+ `expects` called once --- Verify-f `shouldBeCalled` "input"+-- Execute+f "input" ``` ---@@ -61,7 +65,7 @@ | | **Before: Handwritten...** 😫 | **After: Mockcat** 🐱✨ | | :--- | :--- | :--- | | **Definition (Stub)**<br />"I want to return<br />this value for this arg" | <pre>f :: String -> IO String<br />f arg = case arg of<br /> "a" -> pure "b"<br /> _ -> error "unexpected"</pre><br />_Even simple branching consumes many lines._ | <pre>-- Use stub if verification is unneeded (Pure)<br />let f = stub ("a" ~> "b")</pre><br />_Behaves as a completely pure function._ |-| **Verification (Verify)**<br />"I want to test<br />if it was called correctly" | <pre>-- Need to implement recording logic<br />ref <- newIORef []<br />let f arg = do<br /> modifyIORef ref (arg:)<br /> ...<br /><br />-- Verification logic<br />calls <- readIORef ref<br />calls `shouldBe` ["a"]</pre><br />_※ This is just one example. Real-world setups often require even more boilerplate._ | <pre>-- Use mock if verification is needed (Recorded internally)<br />f <- mock ("a" ~> "b")<br /><br />-- Just state what you want to verify<br />f `shouldBeCalled` "a"</pre><br />_Recording is automatic.<br />Focus on the "Why" and "What", not the "How"._ |+| **Verify**<br />(Did it get called<br />correctly?) | <pre>-- Need manual recording mechanism<br />ref <- newIORef []<br />let f arg = do<br /> modifyIORef ref (arg:)<br /> ...<br /><br />-- Verification Logic<br />calls <- readIORef ref<br />calls `shouldBe` ["a"]</pre><br />_ This is just one example. Boilerplate often grows._ | <pre>withMock $ do<br /> -- Declare expected values at definition<br /> f <- mock ("a" ~> "b")<br /> `expects` called once<br /><br /> -- Just execute (Automatic verification)</pre><br />_Recording is automatic.<br />Focus on the "Why" and "What", not the "How"._ | ### Key Features @@ -107,72 +111,90 @@ ### First Test (`Main.hs` / `Spec.hs`) ```haskell-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE TypeApplications #-} import Test.Hspec import Test.MockCat -main :: IO ()-main = hspec spec- spec :: Spec spec = do- it "Quick Start Demo" do- -- 1. Create a mock (Return 42 when receiving "Hello")- f <- mock ("Hello" ~> (42 :: Int))+ it "Quick Start Demo" $ do+ withMockIO $ do+ -- 1. Create a mock (Return 42 when receiving "Hello")+ -- Simultaneously declare "it should be called once"+ f <- mock ("Hello" ~> (42 :: Int))+ `expects` called once - -- 2. Use it as a function- let result = f "Hello"- result `shouldBe` 42+ -- 2. Call f "Hello" and get 42+ f "Hello" `shouldBe` 42 - -- 3. Verify it was called- f `shouldBeCalled` "Hello"+ -- 3. Verification happens automatically when exiting the scope ``` ---- -### At a Glance: Matchers-| Matcher | Description | Example |-| :--- | :--- | :--- |-| **`any`** | Matches any value | `f <- mock (any ~> True)` |-| **`when`** | Matches condition | `f <- mock (when (> 5) "gt 5" ~> True)` |-| **`"val"`** | Matches value (Eq) | `f <- mock ("val" ~> True)` |-| **`inOrder`** | Order verification | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |-| **`inPartial`**| Partial order | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` | ----- ## User Guide Mockcat supports two verification styles depending on your testing needs and preferences. -1. **Post-Verification Style (Spy)**:- Define mock behavior, run the code, and verify afterwards using `shouldBeCalled`. - Ideal for exploratory testing or simple setups. (Used mainly in Sections 1 & 2 below)-2. **Pre-Expectation Style (Declarative/Expectation)**:- Describe "how it should be called" at the definition time. - Ideal for strict interaction testing. (Explained in Section 3)--### 1. Function Mocking (`mock`) - [Basic]+### 1. Declarative Verification (`withMock` / `expects`) - [Recommended] -The most basic usage. Creates a function that returns values for specific arguments.+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. ```haskell--- Function that returns True for "a" -> "b"-f <- mock ("a" ~> "b" ~> True)+import Test.Hspec+import Test.MockCat+import Control.Monad.IO.Class (MonadIO(liftIO))++spec :: Spec+spec = do+ it "User Guide (withMock)" $ do+ withMock $ do+ -- Define a mock that returns True for "Hello"+ f <- mock ("Hello" ~> True)+ `expects` called once++ -- Execution+ let result = f "Hello"++ liftIO $ result `shouldBe` True ``` -**Flexible Matching**:-You can specify conditions (predicates) instead of concrete values.+#### `withMockIO`: Simplified IO Testing+`withMockIO` is an IO-specialized version of `withMock`. It allows you to run IO actions directly within the mock context without needing `liftIO`. ```haskell--- Arbitrary string (param any)-f <- mock (any ~> True)+import Test.Hspec+import Test.MockCat --- Condition (when)-f <- mock (when (> 5) "> 5" ~> True)+spec :: Spec+spec = do+ it "User Guide (withMockIO)" $ do+ withMockIO $ do+ f <- mock ("Hello" ~> True)+ `expects` called once++ let result = f "Hello"++ result `shouldBe` True ``` +> [!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.+>+> ❌ `mock $ any ~> True expects ...`+> ✅ `mock (any ~> True) expects ...`++> [!NOTE]+> You can also use `expects` for declarative verification inside `runMockT` blocks.+> This works seamlessly with generated typeclass mocks as well.+>+> ```haskell+> runMockT do+> _readFile ("config.txt" ~> pure "value")+> `expects` called once+> ```+ ### 2. Typeclass Mocking (`makeMock`) Useful when you want to bring existing typeclasses directly into your tests. Generates mocks from existing typeclasses using Template Haskell.@@ -223,48 +245,54 @@ result `shouldBe` () ``` -### 3. Declarative Verification (`withMock` / `expects`)+### 3. Function Mocking and Post-Verification (`mock` / `shouldBeCalled`) -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.+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. ```haskell-withMock $ do- -- Write expectations (expects) at definition time- f <- mock (any ~> True)- `expects` do- called once `with` "arg"+import Test.Hspec+import Test.MockCat - -- Execution- f "arg"+spec :: Spec+spec = do+ it "Function Mocking" $ do+ -- Define a mock that returns True for "Hello" (No 'expects' here)+ f <- mock ("Hello" ~> True)+ + -- Execution+ f "Hello" `shouldBe` True -#### `withMockIO`: Simplified IO Testing-`withMockIO` is an IO-specialized version of `withMock`. It allows you to run IO actions directly within the mock context without needing `liftIO`.+ -- Post-Verification (shouldBeCalled)+ f `shouldBeCalled` "Hello"+``` +> [!WARNING]+> **Limitation in HPC (Code Coverage) Environments**+> Do not use `shouldBeCalled` when running tests with `stack test --coverage` or similar.+> The code coverage instrumentation by GHC wraps functions, which changes their identity and causes verification to fail.+> If you need code coverage, please use the **`expects`** style (Section 1).++**Flexible Matching**:+You can specify conditions (predicates) instead of concrete values.+ ```haskell-it "IO test" $ withMockIO do- f <- mock (any ~> pure "result")- res <- someIOCall f- res `shouldBe` "result"-```-```+{-# LANGUAGE TypeApplications #-}+import Test.Hspec+import Test.MockCat+import Prelude hiding (any) -> [!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.->-> ❌ `mock $ any ~> True expects ...`-> ✅ `mock (any ~> True) expects ...`+spec :: Spec+spec = do+ it "Matcher Examples" $ do+ -- Arbitrary string (param any)+ f <- mock (any @String ~> True)+ f "foo" `shouldBe` True -> [!NOTE]-> You can also use `expects` for declarative verification inside `runMockT` blocks.-> This works seamlessly with generated typeclass mocks as well.->-> ```haskell-> runMockT do-> _readFile "config.txt" ~> pure "value"-> `expects` called once-> ```+ -- Condition (when)+ g <- mock (when (> (5 :: Int)) "> 5" ~> True)+ g 6 `shouldBe` True+``` ### 4. Flexible Verification (Matchers) @@ -402,28 +430,6 @@ ※ Use this section as a dictionary when you get stuck. -### Verification Matchers (`shouldBeCalled`)--| Matcher | Description | Example |-| :--- | :--- | :--- |-| `x` (Value itself) | Was called with that value | ``f `shouldBeCalled` (10 :: Int)`` |-| `times n` | Exact count | ``f `shouldBeCalled` (times 3 `with` "arg")`` |-| `once` | Exactly once | ``f `shouldBeCalled` (once `with` "arg")`` |-| `never` | Never called | ``f `shouldBeCalled` never`` |-| `atLeast n` | n or more times | ``f `shouldBeCalled` atLeast 2`` |-| `atMost n` | n or fewer times | ``f `shouldBeCalled` atMost 5`` |-| `anything` | Any argument (count ignored) | ``f `shouldBeCalled` anything`` |-| `inOrderWith [...]` | Strict order | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |-| `inPartialOrderWith [...]` | Partial order (skips allowed) | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` |--### Parameter Matchers (Definition)--| Matcher | Description | Example |-| :--- | :--- | :--- |-| `any` | Any value | `any ~> True` |-| `when pred label` | Condition | `when (>0) "positive" ~> True` |-| `when_ pred` | No label | `when_ (>0) ~> True` |- ### Declarative Verification DSL (`expects`) In `expects` blocks, you can describe expectations declaratively using a builder-style syntax.@@ -458,6 +464,28 @@ | **`with matcher`** | Uses a matcher for argument verification. | `called `with` when (>5) "gt 5"` | | **`inOrder`** | Verify usage order (when used in a list) | (See "Order Verification" section) | +### Verification Matchers (`shouldBeCalled`)++| Matcher | Description | Example |+| :--- | :--- | :--- |+| `x` (Value itself) | Was called with that value | ``f `shouldBeCalled` (10 :: Int)`` |+| `times n` | Exact count | ``f `shouldBeCalled` (times 3 `with` "arg")`` |+| `once` | Exactly once | ``f `shouldBeCalled` (once `with` "arg")`` |+| `never` | Never called | ``f `shouldBeCalled` never`` |+| `atLeast n` | n or more times | ``f `shouldBeCalled` atLeast 2`` |+| `atMost n` | n or fewer times | ``f `shouldBeCalled` atMost 5`` |+| `anything` | Any argument (count ignored) | ``f `shouldBeCalled` anything`` |+| `inOrderWith [...]` | Strict order | ``f `shouldBeCalled` inOrderWith ["a", "b"]`` |+| `inPartialOrderWith [...]` | Partial order (skips allowed) | ``f `shouldBeCalled` inPartialOrderWith ["a", "c"]`` |++### Parameter Matchers (Definition)++| Matcher | Description | Example |+| :--- | :--- | :--- |+| `any` | Any value | `any ~> True` |+| `when pred label` | Condition | `when (>0) "positive" ~> True` |+| `when_ pred` | No label | `when_ (>0) ~> True` |+ ### FAQ <details>@@ -472,7 +500,9 @@ <details> <summary><strong>Q. Can I run tests with code coverage (HPC)?</strong></summary>-A. Yes (since v1.1.0.0). Mockcat natively handles the instability of `StableName` introduced by HPC instrumentation, so you can run `stack test --coverage` without issues.+A. Yes (since v1.1.0.0). Mockcat's `expects` style is designed to be unaffected by the function wrapping performed by HPC, so it operates safely even under HPC.+However, for the reasons mentioned above, we strongly recommend using the **`expects`** style (or `withMock`).+The `shouldBeCalled` style cannot be used because HPC's mechanism makes it impossible to identify mock identity. </details> <details>
mockcat.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: mockcat-version: 1.3.3.0+version: 1.4.0.0 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.@@ -38,7 +38,6 @@ library exposed-modules: Test.MockCat- Test.MockCat.AssociationList Test.MockCat.Cons Test.MockCat.Internal.Builder Test.MockCat.Internal.GHC.StableName@@ -89,17 +88,15 @@ Property.ParamSpecRangeMergeRandomProp Property.ReinforcementProps Property.ScriptProps- ReadmeVerifySpec Support.ParamSpec Support.ParamSpecNormalize- Test.MockCat.AssociationListSpec Test.MockCat.ConcurrencySpec Test.MockCat.ConfirmTHSpec Test.MockCat.ConsSpec Test.MockCat.DeriveSpec Test.MockCat.ExampleSpec- Test.MockCat.HPCFallbackSpec Test.MockCat.HPCNestSpec+ Test.MockCat.HpcSpec Test.MockCat.Impl Test.MockCat.Internal.MockRegistrySpec Test.MockCat.MockSpec@@ -109,11 +106,20 @@ Test.MockCat.PartialMockCommonSpec Test.MockCat.PartialMockSpec Test.MockCat.PartialMockTHSpec+ Test.MockCat.Readme.MakeAutoLiftMockSpec+ Test.MockCat.Readme.MakeMockSpec+ Test.MockCat.Readme.MatcherSpec+ Test.MockCat.Readme.QuickStartDemoSpec+ Test.MockCat.Readme.ReadmeSpec+ Test.MockCat.Readme.ShouldBeCalledSpec+ Test.MockCat.Readme.WithMockIOSpec+ Test.MockCat.Readme.WithMockSpec Test.MockCat.SharedSpecDefs Test.MockCat.ShouldBeCalledErrorDiffSpec Test.MockCat.ShouldBeCalledMockMSpec Test.MockCat.ShouldBeCalledSpec Test.MockCat.StubSpec+ Test.MockCat.TestHelper Test.MockCat.TH.ClassAnalysisSpec Test.MockCat.TH.ContextBuilderSpec Test.MockCat.TH.FunctionBuilderSpec
− src/Test/MockCat/AssociationList.hs
@@ -1,34 +0,0 @@-module Test.MockCat.AssociationList - (AssociationList, empty, insert, lookup, member, (!?), update) where--import Prelude hiding (lookup)-import Data.Maybe (isJust)--type AssociationList k a = [(k, a)]--empty :: AssociationList k a-empty = []--insert :: Eq k => k -> a -> AssociationList k a -> AssociationList k a-insert key value [] = [(key, value)]-insert key value ((k, v) : xs)- | key == k = (key, value) : xs- | otherwise = (k, v) : insert key value xs--lookup :: Eq k => k -> AssociationList k a -> Maybe a-lookup _ [] = Nothing-lookup key ((k, a) : xs)- | key == k = Just a- | otherwise = lookup key xs--member :: Eq k => k -> AssociationList k a -> Bool-member k list = isJust (lookup k list)--(!?) :: Eq k => AssociationList k a -> k -> Maybe a-(!?) = flip lookup--update :: Eq k => (a -> a) -> k -> AssociationList k a -> AssociationList k a-update f key list =- case list !? key of- Just value -> insert key (f value) list- Nothing -> list
src/Test/MockCat/Internal/Builder.hs view
@@ -29,7 +29,6 @@ import Data.Maybe import Test.MockCat.Cons (Head(..), (:>)(..)) import Test.MockCat.Param-import Test.MockCat.AssociationList (lookup, update, insert, empty, member) import Prelude hiding (lookup) import Control.Monad.State import Test.MockCat.Internal.Types@@ -155,20 +154,6 @@ pure (BuiltMock fn recorder) ---- | Instance for building a stub for a value (backward compatibility).-instance- MockBuilder (Param r) r ()- where- build _ params = do- ref <- liftIO $ newTVarIO invocationRecord- let v = value params- fn = perform $ do- liftIO $ appendCalledParams ref ()- pure v- recorder = InvocationRecorder ref PureConstant- pure (BuiltMock fn recorder)- -- | Instance for building a stub for `Cases (IO a) ()`. instance MockBuilder (Cases (IO a) ()) (IO a) () where build _ cases = do@@ -257,7 +242,7 @@ invocationRecord = InvocationRecord { invocations = mempty- , invocationCounts = empty+ , invocationCounts = [] } appendCalledParams :: TVar (InvocationRecord params) -> params -> IO ()@@ -268,23 +253,18 @@ { invocations = invocations record ++ [inputParams] } -readInvocationCount :: Eq params => TVar (InvocationRecord params) -> params -> IO Int+readInvocationCount :: EqParams params => TVar (InvocationRecord params) -> params -> IO Int readInvocationCount ref params = do record <- readTVarIO ref- pure $ fromMaybe 0 (lookup params (invocationCounts record))+ pure $ fromMaybe 0 (lookupEqParams params (invocationCounts record)) -incrementInvocationCount :: Eq params => TVar (InvocationRecord params) -> params -> IO ()+incrementInvocationCount :: EqParams params => TVar (InvocationRecord params) -> params -> IO () incrementInvocationCount ref inputParams = atomically $ modifyTVar' ref $ \record -> record- { invocationCounts = incrementCount inputParams (invocationCounts record)+ { invocationCounts = incrementCountEqParams inputParams (invocationCounts record) }--incrementCount :: Eq k => k -> InvocationCounts k -> InvocationCounts k-incrementCount key list =- if member key list then update (+ 1) key list- else insert key 1 list runCase :: Cases a b -> [a] runCase (Cases s) = execState s []
src/Test/MockCat/Internal/MockRegistry.hs view
@@ -13,7 +13,6 @@ , UnitMeta , withUnitGuard , withAllUnitGuards- , markUnitUsed , isGuardActive , getLastRecorder , resetMockHistory@@ -27,7 +26,6 @@ , UnitMeta , withUnitGuard , withAllUnitGuards- , markUnitUsed , isGuardActive , getLastRecorder , resetMockHistory@@ -52,8 +50,8 @@ -- | Wrap a function value for unit-typed stubs so that calls are tracked. -- This uses the UnitMeta guard to avoid double-counting when both the tracked--- and base values are registered. The wrapper will mark the unit meta used and--- append an invocation to the recorder's TVar when appropriate.+-- and base values are registered. The wrapper will append an invocation to the+-- recorder's TVar when appropriate. wrapUnitStub :: forall fn. Typeable fn =>@@ -67,7 +65,6 @@ if guardActive || isIOType (Proxy :: Proxy fn) then pure value else do- markUnitUsed meta appendCalledParams ref () pure value in
src/Test/MockCat/Internal/Registry/Core.hs view
@@ -3,21 +3,18 @@ {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ExistentialQuantification #-}+{- HLINT ignore "Use newtype instead of data" -} module Test.MockCat.Internal.Registry.Core ( attachVerifierToFn , lookupVerifierForFn , attachDynamicVerifierToFn- , createOverlay- , installOverlay- , clearOverlay , registerUnitMeta , lookupUnitMeta , UnitMeta , withUnitGuard , withAllUnitGuards- , markUnitUsed , isGuardActive , getLastRecorder , getLastRecorderRaw@@ -153,8 +150,6 @@ tid <- myThreadId atomically $ modifyTVar' threadWithMockStore (Map.delete tid) -- attachVerifierToFn :: forall fn params. (Typeable (InvocationRecorder params)) =>@@ -181,15 +176,7 @@ case mbMatch of Just match -> pure [match]- Nothing -> do- -- 2. Fallback: return thread's mock history- -- This handles case where StableName is unstable (e.g. HPC enabled)- tid <- myThreadId- atomically $ do- hist <- readTVar threadMockHistory- case Map.lookup tid hist of- Nothing -> pure []- Just list -> pure list+ Nothing -> pure [] attachDynamicVerifierToFn :: forall fn. fn -> (Maybe MockName, Dynamic) -> IO () attachDynamicVerifierToFn fn (name, payload) = do@@ -223,17 +210,10 @@ | sameFnStable target (stableFnName entry) = Just (mockName entry, entryPayload entry) | otherwise = findMatch target rest ------ type UnitStableName = SomeStableName data UnitMeta = UnitMeta { unitGuardRef :: TVar Bool- , unitUsedRef :: TVar Bool } data UnitEntry = UnitEntry !UnitStableName !UnitMeta@@ -255,22 +235,6 @@ unitRegistry :: TVar UnitRegistry unitRegistry = (unsafePerformIO $ newTVarIO IntMap.empty) :: TVar UnitRegistry --- | Per-run overlay registry (optional).-data Overlay = Overlay---- | Run the given IO action with a per-run overlay registry active.--- The overlay is cleaned up after the action completes.-createOverlay :: IO Overlay-createOverlay = pure Overlay--installOverlay :: Overlay -> IO ()-installOverlay _ = pure ()--clearOverlay :: IO ()-clearOverlay = pure ()--- registerUnitMeta :: TVar ref -> IO UnitMeta registerUnitMeta ref = do stable <- makeStableName ref@@ -308,17 +272,13 @@ withAllUnitGuards :: IO a -> IO a withAllUnitGuards = bracket_ (setAllUnitGuards True) (setAllUnitGuards False) -markUnitUsed :: UnitMeta -> IO ()-markUnitUsed meta = atomically $ writeTVar (unitUsedRef meta) True- isGuardActive :: UnitMeta -> IO Bool-isGuardActive meta = readTVarIO (unitGuardRef meta)+isGuardActive (UnitMeta guardRef) = readTVarIO guardRef createUnitMeta :: IO UnitMeta createUnitMeta = do guardRef <- newTVarIO False- usedRef <- newTVarIO False- pure UnitMeta {unitGuardRef = guardRef, unitUsedRef = usedRef}+ pure $ UnitMeta guardRef findUnit :: UnitStableName -> [UnitEntry] -> Maybe UnitMeta findUnit _ [] = Nothing@@ -332,5 +292,3 @@ store <- readTVar unitRegistry forM_ (concat (IntMap.elems store)) $ \entry -> writeTVar (unitGuardRef (unitEntryMeta entry)) flag--
src/Test/MockCat/Internal/Types.hs view
@@ -15,9 +15,10 @@ import Control.Concurrent.STM (TVar) import Data.Maybe (listToMaybe) import GHC.IO (unsafePerformIO)-import Test.MockCat.AssociationList (AssociationList) import Prelude hiding (lookup) import Control.Monad.State ( State, MonadState, execState, modify )++type AssociationList k a = [(k, a)] type MockName = String
src/Test/MockCat/MockT.hs view
@@ -162,16 +162,12 @@ , envWithMockContext = withMockCtx , envNameForwarders = fwdRef }- -- Run user code with a per-run overlay registry active so registry writes/read- -- during this MockT invocation are isolated to this run.- overlay <- liftIO Registry.createOverlay- liftIO $ Registry.installOverlay overlay+ -- Run user code. liftIO $ Registry.setThreadWithMockContext withMockCtx a <- runReaderT r env actions <- liftIO $ readTVarIO expectsVar liftIO $ sequence_ actions liftIO Registry.clearThreadWithMockContext- liftIO Registry.clearOverlay pure a instance MonadIO m => MonadMockDefs (MockT m) where
src/Test/MockCat/Verify.hs view
@@ -174,28 +174,19 @@ verificationFailureMessage = intercalate "\n"- [ "Error: 'shouldBeCalled' can only verify functions created by 'mock'.",- "",- "The value you passed could not be recognized as a mock function.",- "",- "This usually happens in one of the following cases:",- " - You passed a normal (non-mock) function.",- " - You passed a stub or value not created via 'mock' / 'mockIO'.",- " - You are trying to verify a value that was never registered as a mock.",+ [ "Error: Mock verification failed.", "",- "How to fix it:",- " 1. Make sure you created the function with 'mock' (or 'mockIO' for IO)",- " before calling 'shouldBeCalled'.",- " 2. Pass that mock value directly to 'shouldBeCalled'",- " (not the original function or a plain value).",+ "The function passed to 'shouldBeCalled' could not be recognized as a registered mock.", "",- "If this message still appears, check that:",- " - You are not passing a pure constant.",- " - The mock value is still in scope where 'shouldBeCalled' is used.",+ "Possible causes:",+ " 1. You passed a raw wrapper function around the mock.",+ " 2. You passed a normal (non-mock) function.",+ " 3. HPC (Coverage) is enabled. (HPC instrumentation changes function identity)", "",- "Tip: If you prefer automatic verification,",- "consider using 'withMock', which runs all expectations at the end",- "of the block."+ "Solution:",+ " - If you are using HPC or Wrappers, please use 'expects' or 'withMock' style.",+ " 'expects' is robust against HPC and wrappers as it does not rely on function identity lookup.",+ " - Ensure you are passing the mock function directly created by 'mock'." ] -- ============================================
test/Property/AdditionalProps.hs view
@@ -7,22 +7,32 @@ {-# OPTIONS_GHC -fno-hpc #-} {-# OPTIONS_GHC -Wno-deprecations #-} module Property.AdditionalProps- ( prop_predicate_param_match_counts+ ( spec+ , prop_predicate_param_match_counts , prop_multicase_progression , prop_runMockT_isolation , prop_partial_order_duplicates ) where -import Test.QuickCheck+import Test.QuickCheck hiding (once) import Test.QuickCheck.Monadic (monadicIO, run, assert) import Control.Exception (try, SomeException, evaluate) import Control.Monad (forM, forM_) import Data.List (nub)-import Data.Proxy (Proxy(..))+ import Test.MockCat import Control.Monad.IO.Class (liftIO) import Property.Generators (resetMockHistory)+import Test.Hspec (Spec, describe, it) +spec :: Spec+spec = do+ describe "Property Additional (Predicate / Multi-case / Isolation / Duplicates)" $ do+ it "predicate param counts match" $ property prop_predicate_param_match_counts+ it "multi-case progression saturates" $ property prop_multicase_progression+ it "runMockT isolation of counts" $ property prop_runMockT_isolation+ it "partial order with duplicates behaves" $ property prop_partial_order_duplicates+ perCall :: Int -> a -> a perCall i x = case i of _ -> x@@ -33,19 +43,26 @@ -- | Property: A predicate Param (expect even) accepts only matching values and -- the total recorded calls equals count of even inputs attempted.+-- | Property: A predicate Param (expect even) accepts only matching values and+-- the total recorded calls equals count of even inputs attempted. prop_predicate_param_match_counts :: Property prop_predicate_param_match_counts = forAll genVals $ \xs -> monadicIO $ do- run resetMockHistory- -- build predicate mock: (Int -> Bool) returning True for all evens- f <- run $ mock (expect even "even" ~> True)- results <- run $ mapM (\x -> try (evaluate (f x)) :: IO (Either SomeException Bool)) xs- let successes = length [ () | Right _ <- results ]- expected = length (filter even xs)- -- sanity: every success came from an even value- assert (all even [ x | (x, Right _) <- zip xs results ])- -- counts should match- run $ f `shouldBeCalled` times expected- assert (successes == expected)+ -- Use withMock+ let expected = length (filter even xs)+ run $ withMock $ do+ -- build predicate mock+ f <- mock (expect even "even" ~> True)+ `expects` called (times expected)+ + results <- liftIO $ mapM (\x -> try (evaluate (f x)) :: IO (Either SomeException Bool)) xs+ + let successes = length [ () | Right _ <- results ]+ -- sanity checks (optional, but runMockT handles main verification)+ liftIO $ do+ if successes /= expected + then error $ "Success count mismatch: " ++ show successes ++ " vs " ++ show expected+ else pure ()+ assert True where genVals = resize 40 $ listOf (arbitrary :: Gen Int) @@ -57,23 +74,23 @@ -- saturate at the last value. prop_multicase_progression :: Property prop_multicase_progression = forAll genSeq $ \(arg, rs, extra) -> monadicIO $ do- run resetMockHistory- f <- run $ mock $ cases [ param arg ~> r | r <- rs ] let totalCalls = length rs + extra- -- NOTE [GHC9.4 duplicate-call counting]- -- On GHC 9.4 we observed that using @replicateM totalCalls (evaluate (f arg))@- -- can result in only a single side‑effect (call recording) when all- -- of (argument,result) pairs are identical. The optimizer (or full laziness- -- even under -O0) may float the pure expression @f arg@ and share it.- -- We intentionally inject the loop index via a seq so each iteration has a- -- syntactically distinct thunk, ensuring the unsafePerformIO body runs per call.- vals <- run $- forM [1 .. totalCalls] $ \i ->- evaluate (perCall i (f arg))- let (prefix, suffix) = splitAt (length rs) vals- assert (prefix == rs)- assert (all (== last rs) suffix)- run $ f `shouldBeCalled` times totalCalls+ run $ withMock $ do+ f <- mock (cases [ param arg ~> r | r <- rs ])+ `expects` called (times totalCalls)+ + -- Run loop+ vals <- liftIO $+ forM [1 .. totalCalls] $ \i ->+ evaluate (perCall i (f arg))+ + liftIO $ do+ let (prefix, suffix) = splitAt (length rs) vals+ if prefix /= rs || not (all (== last rs) suffix)+ then error "Result sequence mismatch"+ else pure ()+ + assert True where genSeq = do arg <- arbitrary :: Gen Int@@ -93,18 +110,23 @@ prop_runMockT_isolation = monadicIO $ do run resetMockHistory -- Run 1: expect one call- r1 <- run $ try $ runMockT $ do- f <- liftIO $ mock (param (1 :: Int) ~> True)- addDefinition Definition { symbol = Proxy @"iso", mockFunction = f, verification = Verification (\m' -> m' `shouldBeCalled` times 1) }+ r1 <- run ((try $ runMockT $ do+ f <- mock (param (1 :: Int) ~> True)+ `expects` called once liftIO $ f 1 `seq` pure ()+ ) :: IO (Either SomeException ()))+ case r1 of Left (_ :: SomeException) -> assert False Right () -> assert True+ -- Run 2: expect zero (if leaked, would see 1 and fail)- r2 <- run $ try $ runMockT $ do- f <- liftIO $ mock (param (1 :: Int) ~> True)- addDefinition Definition { symbol = Proxy @"iso", mockFunction = f, verification = Verification (\m' -> m' `shouldBeCalled` times 0) }+ r2 <- run ((try $ runMockT $ do+ _f <- mock (param (1 :: Int) ~> True)+ `expects` called never pure ()+ ) :: IO (Either SomeException ()))+ case r2 of Left (_ :: SomeException) -> assert False Right () -> assert True@@ -118,19 +140,24 @@ -- list of first occurrences; reversing that list (when length >=2) fails. prop_partial_order_duplicates :: Property prop_partial_order_duplicates = forAll genDupScript $ \xs -> length xs >= 2 ==> monadicIO $ do- run resetMockHistory- -- build mock over sequence- f <- run $ mock $ cases [ param x ~> True | x <- xs ]- run $ forM_ xs $ \x -> f x `seq` pure () let uniques = nub xs -- success case- run $ f `shouldBeCalled` inPartialOrderWith (param <$> uniques)+ run $ withMock $ do+ f <- mock (cases [ param x ~> True | x <- xs ])+ `expects` calledInSequence uniques+ liftIO $ forM_ xs $ \x -> f x `seq` pure () assert True+ -- failure case (if we have at least two unique values) case uniques of (_:_:_) | isClusterOrdered uniques xs -> do let reversed = reverse uniques- e <- run (try (f `shouldBeCalled` inPartialOrderWith (param <$> reversed)) :: IO (Either SomeException ()))+ e <- run ((try $ withMock $ do+ f <- mock (cases [ param x ~> True | x <- xs ])+ `expects` calledInSequence reversed+ liftIO $ forM_ xs $ \x -> f x `seq` pure ()+ ) :: IO (Either SomeException ()))+ case e of Left _ -> assert True Right _ -> assert False
test/Property/ConcurrentCountProp.hs view
@@ -8,7 +8,7 @@ {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-missing-export-lists #-} {-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-}-module Property.ConcurrentCountProp where+module Property.ConcurrentCountProp (spec, prop_concurrent_total_apply_count) where import Test.QuickCheck import Test.QuickCheck.Monadic (monadicIO, run, assert)@@ -17,7 +17,9 @@ import Control.Monad.IO.Unlift (withRunInIO, MonadUnliftIO) import Test.MockCat hiding (any) import qualified Test.MockCat as MC (any)+import Test.Hspec (Spec, describe, it) + -- Class for local concurrent actions (defined independently instead of reusing existing ConcurrencySpec) class Monad m => PropConcurrencyAction m where propAction :: Int -> m Int@@ -46,3 +48,8 @@ parallelInvoke threads callsPerThread = withRunInIO $ \runInIO -> do as <- replicateM threads (async $ runInIO $ replicateM_ callsPerThread (propAction 42 >> pure ())) mapM_ wait as++spec :: Spec+spec = do+ describe "Property Concurrency" $ do+ it "total apply count is preserved across threads" $ property prop_concurrent_total_apply_count
test/Property/LazyEvalProp.hs view
@@ -8,12 +8,15 @@ {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-missing-export-lists #-} {-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-}-module Property.LazyEvalProp where+module Property.LazyEvalProp (spec, prop_lazy_unforced_not_counted, prop_lazy_forced_counted) where import Test.QuickCheck hiding (once) import Test.QuickCheck.Monadic (monadicIO, run, assert) import Test.MockCat hiding (any)+import Test.Hspec (Spec, describe, it) ++ -- | Unary action so we can register a concrete expected argument & return value. -- We test that constructing (but not forcing) the call does not count. class Monad m => LazyUnaryAction m where@@ -45,3 +48,9 @@ v <- lazyUnaryAction 10 -- forcing the monadic action executes the mock v `seq` pure () -- ensure result is evaluated (WHNF for Int) assert True++spec :: Spec+spec = do+ describe "Property Lazy Evaluation" $ do+ it "unforced stub action is not counted" $ property prop_lazy_unforced_not_counted+ it "forced stub action is counted" $ property prop_lazy_forced_counted
test/Property/OrderProps.hs view
@@ -1,7 +1,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-hpc #-} module Property.OrderProps- ( prop_inorder_succeeds+ ( spec+ , prop_inorder_succeeds , prop_adjacent_swap_fails , prop_partial_order_subset_succeeds , prop_partial_order_reversed_pair_fails@@ -12,31 +13,48 @@ import Control.Exception (try, SomeException) import Data.Maybe (listToMaybe) import Data.List (nub)-import Test.MockCat (shouldBeCalled, inOrderWith, inPartialOrderWith, param)+-- import Test.MockCat (shouldBeCalled, inOrderWith, inPartialOrderWith, param)+import Test.MockCat import Property.Generators+import Control.Monad.IO.Class (liftIO)+import Test.Hspec (Spec, describe, it) +spec :: Spec+spec = do+ describe "Property Order / PartialOrder" $ do+ it "in-order script succeeds" $ property prop_inorder_succeeds+ it "adjacent swap fails order verification" $ property prop_adjacent_swap_fails+ it "subset partial order succeeds" $ property prop_partial_order_subset_succeeds+ it "reversed pair fails partial order" $ property prop_partial_order_reversed_pair_fails+ -- | Property: executing a non-empty script yields exact order success. prop_inorder_succeeds :: Property prop_inorder_succeeds = forAll scriptGen $ \scr@(Script xs) -> not (null xs) ==> monadicIO $ do- run resetMockHistory- f <- run $ buildUnaryMock scr- run $ runScript f scr- run $ f `shouldBeCalled` inOrderWith (param <$> xs)+ -- Use withMock for safe verification+ run $ withMock $ do+ -- Expect calls in exact order+ f <- mock (cases [ param a ~> True | a <- xs ])+ `expects` calledInOrder xs+ liftIO $ runScript f scr assert True -- | Property: a single adjacent swap causes order verification failure. prop_adjacent_swap_fails :: Property prop_adjacent_swap_fails = forAll scriptGen $ \(Script xs) -> length xs >= 2 ==> monadicIO $ do- run resetMockHistory let distinct = nub xs if length distinct /= length xs then assert True -- discard scripts with duplicates; they can mask order errors else do i <- run $ generate $ chooseInt (0, length xs - 2) let swapped = take i xs ++ [xs !! (i+1), xs !! i] ++ drop (i+2) xs- f <- run $ buildUnaryMock (Script xs)- run $ runScript f (Script xs)- e <- run $ (try (f `shouldBeCalled` inOrderWith (param <$> swapped)) :: IO (Either SomeException ()))+ + -- Verify that expectation fails for swapped order+ e <- run ((try $ withMock $ do+ f <- mock (cases [ param a ~> True | a <- xs ])+ `expects` calledInOrder swapped+ liftIO $ runScript f (Script xs)+ ) :: IO (Either SomeException ()))+ case e of Left _ -> assert True Right _ -> assert False@@ -52,17 +70,16 @@ -- | Property: any non-empty subsequence (order-preserving) passes partial order check. prop_partial_order_subset_succeeds :: Property prop_partial_order_subset_succeeds = forAll scriptGen $ \scr@(Script xs) -> not (null xs) ==> monadicIO $ do- run resetMockHistory subset <- run $ generate $ chooseSubsequence xs- f <- run $ buildUnaryMock scr- run $ runScript f scr- run $ f `shouldBeCalled` inPartialOrderWith (param <$> subset)+ run $ withMock $ do+ f <- mock (cases [ param a ~> True | a <- xs ])+ `expects` calledInSequence subset+ liftIO $ runScript f scr assert True -- | Property: selecting two distinct values and reversing them causes partial order failure. prop_partial_order_reversed_pair_fails :: Property prop_partial_order_reversed_pair_fails = forAll scriptGen $ \scr@(Script xs) -> length xs >= 2 ==> monadicIO $ do- run resetMockHistory if length (nub xs) /= length xs then assert True -- discard non-unique scripts to avoid accidental subsequences else do@@ -70,10 +87,14 @@ case listToMaybe pairs of Nothing -> assert True Just (i,j) -> do- f <- run $ buildUnaryMock scr- run $ runScript f scr let reversed = [xs!!j, xs!!i]- e <- run $ (try (f `shouldBeCalled` inPartialOrderWith (param <$> reversed)) :: IO (Either SomeException ()))+ -- Verify failure+ e <- run ((try $ withMock $ do+ f <- mock (cases [ param a ~> True | a <- xs ])+ `expects` calledInSequence reversed+ liftIO $ runScript f scr+ ) :: IO (Either SomeException ()))+ case e of Left _ -> assert True Right _ -> assert False
test/Property/ReinforcementProps.hs view
@@ -10,8 +10,7 @@ {-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-} {-# OPTIONS_GHC -fno-hpc #-} {-# OPTIONS_GHC -Wno-deprecations #-}-module Property.ReinforcementProps- where+module Property.ReinforcementProps (spec, prop_predicate_negative_not_counted, prop_lazy_partial_force_concurrency, prop_partial_order_interleaved_duplicates) where import Test.QuickCheck import Test.QuickCheck.Monadic (monadicIO, run, assert)@@ -20,29 +19,36 @@ import UnliftIO (withRunInIO) import Test.MockCat hiding (any) import Property.Generators (resetMockHistory)+import Control.Monad.IO.Class (liftIO)+import Test.Hspec (Spec, describe, it) ++ -------------------------------------------------------------------------------- -- 1. Predicate negative: failing inputs raise & are not counted -------------------------------------------------------------------------------- prop_predicate_negative_not_counted :: Property prop_predicate_negative_not_counted = forAll genVals $ \xs -> monadicIO $ do- run resetMockHistory- f <- run $ mock (expect even "even" ~> True)- outcomes <- run $ mapM (\x -> (try (evaluate (f x)) :: IO (Either SomeException Bool))) xs let evens = length (filter even xs)- successes = length [ () | (x, Right _) <- zip xs outcomes, even x ]- oddSuccesses = [ x | (x, Right _) <- zip xs outcomes, odd x ]- failures = length [ () | (_, Left _) <- zip xs outcomes ]- odds = length (filter odd xs)- -- No odd value should succeed- assert (null oddSuccesses)- -- Failures count equals number of odd inputs- assert (failures == odds)- -- Successes count equals even inputs- assert (successes == evens)- -- Called count equals successes (only even)- run $ f `shouldBeCalled` times evens+ run $ withMock $ do+ f <- mock (expect even "even" ~> True)+ `expects` called (times evens)+ + outcomes <- liftIO $ mapM (\x -> (try (evaluate (f x)) :: IO (Either SomeException Bool))) xs+ + {- Analysis (optional checks) -}+ liftIO $ do+ let successes = length [ () | (x, Right _) <- zip xs outcomes, even x ]+ oddSuccesses = [ x | (x, Right _) <- zip xs outcomes, odd x ]+ failures = length [ () | (_, Left _) <- zip xs outcomes ]+ odds = length (filter odd xs)+ -- No odd value should succeed+ if not (null oddSuccesses) then error "oddSuccesses not null" else pure ()+ -- Failures count equals number of odd inputs+ if failures /= odds then error "failures /= odds" else pure ()+ -- Successes count equals even inputs+ if successes /= evens then error "successes /= evens" else pure () assert True where genVals = resize 60 $ listOf (arbitrary :: Gen Int)@@ -88,14 +94,22 @@ prop_partial_order_interleaved_duplicates :: Property prop_partial_order_interleaved_duplicates = forAll genPair $ \(a,b) -> a /= b ==> monadicIO $ do- run resetMockHistory -- Pattern a a b : [a,b] subsequence succeeds, [b,a] fails.- f <- run $ mock $ cases [ param a ~> True- , param a ~> True- , param b ~> True ]- run $ f a `seq` f a `seq` f b `seq` pure ()- run $ f `shouldBeCalled` inPartialOrderWith [param a, param b]- e <- run $ (try (f `shouldBeCalled` inPartialOrderWith [param b, param a]) :: IO (Either SomeException ()))+ run $ withMock $ do+ f <- mock (cases [ param a ~> True+ , param a ~> True+ , param b ~> True ])+ `expects` calledInSequence [a, b]+ liftIO $ f a `seq` f a `seq` f b `seq` pure ()++ e <- run ((try $ withMock $ do+ f <- mock (cases [ param a ~> True+ , param a ~> True+ , param b ~> True ])+ `expects` calledInSequence [b, a]+ liftIO $ f a `seq` f a `seq` f b `seq` pure ()+ ) :: IO (Either SomeException ()))+ case e of Left _ -> assert True Right _ -> assert False@@ -105,3 +119,10 @@ a <- arbitrary :: Gen Int b <- arbitrary :: Gen Int pure (a,b)++spec :: Spec+spec = do+ describe "Property Reinforcement (Negative predicate / Partial force / Interleave)" $ do+ it "predicate negative not counted" $ property prop_predicate_negative_not_counted+ it "lazy partial force in concurrency counts only forced" $ property prop_lazy_partial_force_concurrency+ it "interleaved duplicate partial order semantics" $ property prop_partial_order_interleaved_duplicates
test/Property/ScriptProps.hs view
@@ -1,19 +1,29 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-hpc #-} module Property.ScriptProps- ( prop_script_count_matches+ ( spec+ , prop_script_count_matches ) where import Test.QuickCheck import Test.QuickCheck.Monadic (monadicIO, run, assert)-import Test.MockCat (shouldBeCalled, times)+import Test.MockCat import Property.Generators+import Control.Monad.IO.Class (liftIO)+import Test.Hspec (Spec, describe, it) +spec :: Spec+spec = do+ describe "Property Script Generator" $ do+ it "script count matches recorded calls" $ property prop_script_count_matches+ -- | Property: executing a generated script produces exactly that many recorded calls. prop_script_count_matches :: Property prop_script_count_matches = forAll scriptGen $ \scr@(Script xs) -> monadicIO $ do- run resetMockHistory- f <- run $ buildUnaryMock scr- run $ runScript f scr- run $ f `shouldBeCalled` times (length xs)+ -- Use withMock for safe verification+ run $ withMock $ do+ f <- mock (cases [ param a ~> True | a <- xs ])+ `expects` called (times (length xs))+ + liftIO $ runScript f scr assert True
− test/ReadmeVerifySpec.hs
@@ -1,85 +0,0 @@-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeOperators #-}-{-# OPTIONS_GHC -Wno-missing-export-lists #-}-{-# OPTIONS_GHC -Wno-unused-do-bind #-}-{-# OPTIONS_GHC -fno-hpc #-}-{-# OPTIONS_GHC -Wno-deprecations #-}-module ReadmeVerifySpec where--import Test.Hspec-import Test.MockCat hiding (when)-import Prelude hiding (readFile, writeFile, any)-import Control.Monad (when)----- ---------------------------------------------------------- 2. Typeclass Mocking Definitions--- --------------------------------------------------------class Monad m => FileSystem m where- readFile :: FilePath -> m String- writeFile :: FilePath -> String -> m ()---- [Strict Mode]-makeMock [t|FileSystem|]---- Dummy program for Typeclass Mocking-myProgram :: FileSystem m => FilePath -> m ()-myProgram path = do- content <- readFile path- when (content == "debug=true") $ writeFile "log.txt" "start"---spec :: Spec-spec = do- describe "Quick Start" do- it "Quick Start Demo" do- -- 1. Create a mock- f <- mock $ "Hello" ~> (42 :: Int)-- -- 2. Use it- let result = f "Hello"- result `shouldBe` 42-- {- - Note: The README includes:- -- 3. Verify- f `shouldBeCalled` "Hello"-- Without OverloadedStrings, "Hello" is String.- f :: String -> Int.- shouldBeCalled expects Matcher (String -> Int).- "Hello" works as matcher implies `Param String`.- -}- f `shouldBeCalled` "Hello"-- describe "Function Mocking" do- it "Basic" do- f <- mock $ "a" ~> "b" ~> True- let r = f "a" "b"- r `shouldBe` True-- it "Condition" do- f <- mock $ expect (> 5) "> 5" ~> True- let r = f (6 :: Int)- r `shouldBe` True-- it "Any" do- f <- mock $ any ~> True- let r = f ("something" :: String)- r `shouldBe` True-- describe "Typeclass Mocking" do- it "filesystem test (Strict)" do- result <- runMockT do- _readFile $ "config.txt" ~> pure @IO "debug=true"- _writeFile $ "log.txt" ~> "start" ~> pure @IO ()- myProgram "config.txt"- result `shouldBe` ()
test/Spec.hs view
@@ -1,13 +1,14 @@-import Test.Hspec (hspec, describe, it)+{-# LANGUAGE BlockArguments #-}+import Test.Hspec (hspec, describe, it, pendingWith)+import Test.MockCat.TestHelper (checkStrictVerificationWorks)+ import Test.MockCat.MockSpec as Mock import Test.MockCat.DeriveSpec as Derive import Test.MockCat.ConsSpec as Cons import Test.MockCat.ParamSpec as Param-import Test.MockCat.AssociationListSpec as AssociationList import Test.MockCat.ExampleSpec as Example import Test.MockCat.TypeClassMinimalSpec as TypeClassMinimal import Test.MockCat.TypeClassSpec as TypeClass- import Test.MockCat.TypeClassTHSpec as TypeClassTH import Test.MockCat.PartialMockSpec as PartialMock import Test.MockCat.PartialMockTHSpec as PartialMockTH@@ -26,12 +27,10 @@ import Test.MockCat.ShouldBeCalledErrorDiffSpec as ShouldBeCalledErrorDiff import Test.MockCat.WithMockErrorDiffSpec as WithMockErrorDiff import Test.MockCat.THCompareSpec as THCompare-import ReadmeVerifySpec as ReadmeVerify-import qualified Test.MockCat.HPCFallbackSpec as HPCFallback import qualified Test.MockCat.MultipleMocksSpec as MultipleMocks import qualified Test.MockCat.TypeFamilySpec as TypeFamily import Test.MockCat.UnsafeCheck ()-import Test.QuickCheck (property)+import Test.QuickCheck () import qualified Property.ConcurrentCountProp as ConcurrencyProp import qualified Property.LazyEvalProp as LazyEvalProp import qualified Property.ScriptProps as ScriptProps@@ -41,14 +40,27 @@ import qualified Property.ParamSpecNormalizeProp as ParamSpecNormalizeProp import qualified Property.ParamSpecMergeProp as ParamSpecMergeProp import qualified Property.ParamSpecRangeMergeRandomProp as ParamSpecRangeMergeRandomProp+import qualified Test.MockCat.HpcSpec as HpcSpec+import qualified Test.MockCat.Readme.ReadmeSpec as Readme main :: IO ()-main = hspec $ do+main = do+ strictWorks <- checkStrictVerificationWorks+ -- If strict verification works (Standard), verify everything.+ -- If it fails (HPC enabled), skip standard verification tests that would falsely fail.+ let conditionalSpec = if strictWorks + then id + else \_ -> describe "Standard Tests (Skipped due to HPC/Coverage detection)" $ + it "All standard verification tests skipped" $ + pendingWith "Strict Verification disabled (HPC active)"++ hspec $ do+ -- ALWAYS RUN: HpcSpec & Safe Tests (Cons, Param, Mock, Stub, Expects-based, etc.)+ HpcSpec.spec Cons.spec Param.spec Derive.spec Mock.spec- AssociationList.spec Example.spec TypeClassMinimal.spec TypeClass.spec@@ -58,43 +70,33 @@ Concurrency.spec Stub.spec MockTSpec.spec- Registry.spec THCompare.spec THTypeUtils.spec THContextBuilder.spec THClassAnalysis.spec THFunctionBuilder.spec- ShouldBeCalled.spec- ShouldBeCalledMockM.spec WithMock.spec- WithMockIO.spec- ShouldBeCalledErrorDiff.spec WithMockErrorDiff.spec- ReadmeVerify.spec- HPCFallback.spec+ WithMockIO.spec MultipleMocks.spec TypeFamily.spec- describe "Property Concurrency" $ do- it "total apply count is preserved across threads" $ property ConcurrencyProp.prop_concurrent_total_apply_count- describe "Property Lazy Evaluation" $ do- it "unforced stub action is not counted" $ property LazyEvalProp.prop_lazy_unforced_not_counted- it "forced stub action is counted" $ property LazyEvalProp.prop_lazy_forced_counted- describe "Property Script Generator" $ do- it "script count matches recorded calls" $ property ScriptProps.prop_script_count_matches- describe "Property Order / PartialOrder" $ do- it "in-order script succeeds" $ property OrderProps.prop_inorder_succeeds- it "adjacent swap fails order verification" $ property OrderProps.prop_adjacent_swap_fails- it "subset partial order succeeds" $ property OrderProps.prop_partial_order_subset_succeeds- it "reversed pair fails partial order" $ property OrderProps.prop_partial_order_reversed_pair_fails- describe "Property Additional (Predicate / Multi-case / Isolation / Duplicates)" $ do- it "predicate param counts match" $ property AdditionalProps.prop_predicate_param_match_counts- it "multi-case progression saturates" $ property AdditionalProps.prop_multicase_progression- it "runMockT isolation of counts" $ property AdditionalProps.prop_runMockT_isolation- it "partial order with duplicates behaves" $ property AdditionalProps.prop_partial_order_duplicates- describe "Property Reinforcement (Negative predicate / Partial force / Interleave)" $ do- it "predicate negative not counted" $ property ReinforcementProps.prop_predicate_negative_not_counted- it "lazy partial force in concurrency counts only forced" $ property ReinforcementProps.prop_lazy_partial_force_concurrency- it "interleaved duplicate partial order semantics" $ property ReinforcementProps.prop_partial_order_interleaved_duplicates++ ConcurrencyProp.spec+ LazyEvalProp.spec+ ScriptProps.spec+ OrderProps.spec+ AdditionalProps.spec+ ReinforcementProps.spec+ ParamSpecNormalizeProp.spec ParamSpecMergeProp.spec ParamSpecRangeMergeRandomProp.spec++ -- CONDITIONAL RUN: Standard Tests using 'shouldBeCalled' (Unsafe under HPC)+ conditionalSpec $ do+ Registry.spec+ ShouldBeCalled.spec+ ShouldBeCalledMockM.spec+ ShouldBeCalledErrorDiff.spec++ Readme.spec strictWorks
− test/Test/MockCat/AssociationListSpec.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE BlockArguments #-}-{-# OPTIONS_GHC -Wno-type-defaults #-}-module Test.MockCat.AssociationListSpec (spec) where--import Test.Hspec-import Test.MockCat.AssociationList (AssociationList, empty, insert, update, (!?), member)--spec :: Spec-spec = do- it "empty" do- let e = empty :: AssociationList String Int- e `shouldBe` []-- describe "insert" do- it "not exist" do- let l = empty :: AssociationList String Int- insert "key" 100 l `shouldBe` [("key", 100)]-- it "exist" do- let l = insert "key" 100 empty :: AssociationList String Int- insert "key" 120 l `shouldBe` [("key", 120)]- - describe "update" do- it "not exist" do- let l = empty :: AssociationList String Int- update (+ 20) "key" l `shouldBe` []-- it "exist" do- let l = insert "key" 100 empty :: AssociationList String Int- update (+ 20) "key" l `shouldBe` [("key", 120)]- - describe "lookup" do- it "not exist" do- let l = empty :: AssociationList String Int- l !? "key" `shouldBe` Nothing-- it "exist" do- let l = insert "key" 100 empty :: AssociationList String Int- l !? "key" `shouldBe` Just 100-- describe "member" do- it "not exist" do- let l = empty :: AssociationList String Int- member "key" l `shouldBe` False-- it "exist" do- let l = insert "key" 100 empty :: AssociationList String Int- member "key" l `shouldBe` True
− test/Test/MockCat/HPCFallbackSpec.hs
@@ -1,59 +0,0 @@-{-# OPTIONS_GHC -Wno-unused-do-bind #-}-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Test.MockCat.HPCFallbackSpec (spec) where--import Test.Hspec-import Test.MockCat-import Control.Exception (evaluate)-import Prelude hiding (readFile, writeFile)--spec :: Spec-spec = do- describe "HPC Fallback Scenarios (Multiple Mocks)" do- - it "distinguishes mocks of different types" do- -- Case 1: Different types should be distinguishable by type check in fallback- f <- mock $ "a" ~> (1 :: Int)- g <- mock ((1 :: Int) ~> "b")- - -- Verify f (String -> Int)- f "a" `shouldBe` 1- f `shouldBeCalled` "a"- - -- Verify g (Int -> String)- g 1 `shouldBe` "b"- g `shouldBeCalled` (1 :: Int)-- it "behavior with multiple mocks of SAME type (Unnamed)" do- -- Case 2: Same type mocks. - -- In HPC environment (StableName broken), fallback picks based on history.- -- Expected behavior: It might pick the wrong recorder if implementation blindly takes the first/last match.- - f1 <- mock $ "1" ~> (1 :: Int)- f2 <- mock $ "2" ~> (2 :: Int)-- -- Execute both- evaluate $ f1 "1"- evaluate $ f2 "2"-- -- Verify f1. - -- If fallback picks f2 (latest or first), verification might fail or see wrong calls.- f1 `shouldBeCalled` "1" - - -- Verify f2- f2 `shouldBeCalled` "2"-- it "behavior with multiple mocks of SAME type (Named)" do- -- Case 3: Named mocks of same type.- -- Verification should ideally use the name to disambiguate.- - fA <- mock (label "mockA") $ "A" ~> (1 :: Int)- fB <- mock (label "mockB") $ "B" ~> (2 :: Int)-- evaluate $ fA "A"- evaluate $ fB "B"-- fA `shouldBeCalled` "A"- fB `shouldBeCalled` "B"
+ test/Test/MockCat/HpcSpec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+module Test.MockCat.HpcSpec (spec) where++import Test.Hspec+import Test.MockCat+import Control.Exception (ErrorCall (..))+import Control.Monad.IO.Class (liftIO)++spec :: Spec+spec = do+ describe "[HPC] Strict Verification Compatibility" $ do+ it "expects works correctly" $ do+ withMock $ do+ m <- mock ("arg" ~> "result") `expects` (called (times 1))+ let r = m "arg"+ liftIO $ r `shouldBe` "result"++ it "shouldBeCalled fails with specific error message when StableName lookup fails" $ do+ -- In HPC mode (or if wrapped), StableName lookup fails.+ -- To guarantee failure regardless of HPC trigger, we use a Wrapper.+ -- This simulates the condition we want to test: "Mock not recognized".+ + m :: String -> String <- mock $ "arg" ~> "result"+ + -- Wrap the mock. verifying 'wrapped' directly should fail+ -- because 'wrapped' is not registered in the Core registry.+ let wrapped x = m x+ + -- Execute verification on the WRAPPED (unregistered) function.+ (wrapped `shouldBeCalled` "arg") `shouldThrow` \(ErrorCall msg) ->+ let expected = + "Error: Mock verification failed.\n" +++ "\n" +++ "The function passed to 'shouldBeCalled' could not be recognized as a registered mock.\n" +++ "\n" +++ "Possible causes:\n" +++ " 1. You passed a raw wrapper function around the mock.\n" +++ " 2. You passed a normal (non-mock) function.\n" +++ " 3. HPC (Coverage) is enabled. (HPC instrumentation changes function identity)\n" +++ "\n" +++ "Solution:\n" +++ " - If you are using HPC or Wrappers, please use 'expects' or 'withMock' style.\n" +++ " 'expects' is robust against HPC and wrappers as it does not rely on function identity lookup.\n" +++ " - Ensure you are passing the mock function directly created by 'mock'."+ in msg == expected
test/Test/MockCat/Internal/MockRegistrySpec.hs view
@@ -6,7 +6,6 @@ import Test.MockCat.Internal.MockRegistry import Control.Concurrent.STM (newTVarIO, readTVarIO) import Data.Dynamic (fromDynamic)-import Test.MockCat.AssociationList (empty) import Test.MockCat.Internal.Types (InvocationRecorder(..), FunctionNature(..), InvocationRecord(..)) spec :: Spec@@ -14,7 +13,7 @@ describe "MockRegistry" do it "register and lookup" do let f = (+ 1) :: Int -> Int- ref <- newTVarIO InvocationRecord { invocations = [] :: [Int], invocationCounts = empty }+ ref <- newTVarIO InvocationRecord { invocations = [] :: [Int], invocationCounts = [] } attachVerifierToFn f (Just "name", InvocationRecorder ref ParametricFunction) results <- lookupVerifierForFn f case results of@@ -23,7 +22,7 @@ case (fromDynamic dyn :: Maybe (InvocationRecorder Int)) of Just (InvocationRecorder vref _) -> do r <- readTVarIO vref- r `shouldBe` InvocationRecord { invocations = [] :: [Int], invocationCounts = empty }+ r `shouldBe` InvocationRecord { invocations = [] :: [Int], invocationCounts = [] } Nothing -> expectationFailure "payload dynamic mismatch" _ -> expectationFailure "lookupStubFn returned unexpected number of results"
test/Test/MockCat/PartialMockSpec.hs view
@@ -16,7 +16,7 @@ import Data.Text (Text) import Test.Hspec (Spec) import Data.List (find)-import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.IO.Class (MonadIO) import Control.Monad.Trans.Class (lift) import Test.MockCat import Test.MockCat.SharedSpecDefs@@ -28,19 +28,8 @@ import Unsafe.Coerce (unsafeCoerce) import qualified Test.MockCat.Verify as Verify -ensureVerifiable ::- ( MonadIO m- , Verify.ResolvableMock target- ) =>- target ->- m ()-ensureVerifiable target =- liftIO $ do- candidates <- Verify.resolveForVerification target- case candidates of- [] -> Verify.verificationFailure- _ -> pure () + instance UserInputGetter IO where getInput = getLine toUserInput "" = pure Nothing@@ -113,7 +102,7 @@ MockT m (FilePath -> Text) _readFile p = MockT $ do mockInstance <- unMockT $ mock (label "readFile") p- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "readFile") mockInstance NoVerification) pure mockInstance @@ -125,7 +114,7 @@ MockT m (MockResult (Verify.ResolvableParamsOf (FilePath -> Text -> ()))) _writeFile p = MockT $ do mockInstance <- unMockT $ mock (label "writeFile") p- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "writeFile") mockInstance NoVerification) pure (MockResult ()) @@ -138,7 +127,7 @@ MockT m (MockResult (Verify.ResolvableParamsOf String)) _getInput value = MockT $ do mockInstance <- unMockT $ mock (label "getInput") value- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "getInput") mockInstance NoVerification) pure (MockResult ()) @@ -152,7 +141,7 @@ MockT m (String -> m (Maybe UserInput)) _toUserInput p = MockT $ do mockInstance <- unMockT $ mock (label "toUserInput") p- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "toUserInput") mockInstance NoVerification) pure mockInstance @@ -163,7 +152,7 @@ MockT IO (MockResult (Verify.ResolvableParamsOf (String -> IO Int))) _getByPartial p = MockT $ do mockInstance <- unMockT $ mock (label "getBy") p- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "getBy") mockInstance NoVerification) pure (MockResult ()) @@ -174,7 +163,7 @@ MockT IO (MockResult (Verify.ResolvableParamsOf (String -> IO ()))) _echoPartial p = MockT $ do mockInstance <- unMockT $ mock (label "echo") p- ensureVerifiable mockInstance+ addDefinition (Definition (Proxy :: Proxy "echo") mockInstance NoVerification) pure (MockResult ()) @@ -213,7 +202,7 @@ _findIds p = MockT $ do mockInstance <- unMockT $ mock (label "_findIds") p - ensureVerifiable mockInstance+ addDefinition ( Definition (Proxy :: Proxy "_findIds")@@ -230,7 +219,7 @@ MockT m (MockResult (Verify.ResolvableParamsOf (Int -> String))) _findById p = MockT $ do mockInstance <- unMockT $ mock (label "_findById") p- ensureVerifiable mockInstance+ addDefinition ( Definition (Proxy :: Proxy "_findById")@@ -246,7 +235,7 @@ MockT IO (MockResult (Verify.ResolvableParamsOf (Int -> IO String))) _findByIdNI p = MockT $ do mockInstance <- unMockT $ mock (label "_findByIdNI") p- ensureVerifiable mockInstance+ addDefinition ( Definition (Proxy :: Proxy "_findByIdNI")
+ test/Test/MockCat/Readme/MakeAutoLiftMockSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}+module Test.MockCat.Readme.MakeAutoLiftMockSpec (spec) where++import Test.Hspec+import Test.MockCat++class Monad m => FileSystem m where+ readFile :: FilePath -> m String+ writeFile :: FilePath -> String -> m ()++-- [Auto-Lift Mode] 利便性重視のモード。+-- 純粋な値を自動的にモナド(m String など)に包んで返します。+makeAutoLiftMock [t|FileSystem|]++-- Dummy program for makeMock example+myProgram :: FileSystem m => FilePath -> m ()+myProgram _ = pure ()++spec :: Spec+spec = do+ it "filesystem test" $ do+ result <- runMockT $ do+ -- [Auto-Lift Mode] (makeAutoLiftMock 使用時): 値は自動的に包まれる (便利)+ _readFile $ "config.txt" ~> "debug=true"+ _writeFile $ "log.txt" ~> "start" ~> ()++ -- テスト対象コードの実行(モックが注入される)+ myProgram "config.txt"+ + result `shouldBe` ()
+ test/Test/MockCat/Readme/MakeMockSpec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}+module Test.MockCat.Readme.MakeMockSpec (spec) where++import Test.Hspec+import Test.MockCat++class Monad m => FileSystem m where+ readFile :: FilePath -> m String+ writeFile :: FilePath -> String -> m ()++-- [Strict Mode] デフォルトの動作。「mock」関数と挙動が一致します。+-- 戻り値の型が `m a` の場合、スタブ定義の右辺には `m a` 型の値(例: `pure @IO "value"`, `throwIO Error`)を記述する必要があります。+-- Haskell の型システムに対して正直で、明示的な記述を好む場合に推奨されます。+makeMock [t|FileSystem|]++-- Dummy program for makeMock example+myProgram :: FileSystem m => FilePath -> m ()+myProgram _ = pure ()++spec :: Spec+spec = do+ it "filesystem test" $ do+ result <- runMockT $ do+ -- [Strict Mode] (makeMock 使用時): 明示的に pure で包む+ _readFile $ "config.txt" ~> pure @IO "debug=true"+ _writeFile $ "log.txt" ~> "start" ~> pure @IO ()++ -- テスト対象コードの実行(モックが注入される)+ myProgram "config.txt"+ + result `shouldBe` ()
+ test/Test/MockCat/Readme/MatcherSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE TypeApplications #-}+module Test.MockCat.Readme.MatcherSpec (spec) where++import Test.Hspec+import Test.MockCat+import Prelude hiding (any)+import Data.List (isPrefixOf)++spec :: Spec+spec = do+ it "Matcher Examples" $ do+ -- 任意の文字列 (param any)+ f <- mock (any @String ~> True)+ f "foo" `shouldBe` True++ -- 条件式 (when)+ g <- mock (when (> (5 :: Int)) "> 5" ~> True)+ g 6 `shouldBe` True+ + it "When Example" $ do+ -- 引数が "error" で始まる場合のみ False を返す+ f <- mock $ do+ onCase $ when (\s -> "error" `isPrefixOf` s) "start with error" ~> False+ onCase $ any ~> True++ f "error message" `shouldBe` False+ f "success" `shouldBe` True+ f "other" `shouldBe` True
+ test/Test/MockCat/Readme/QuickStartDemoSpec.hs view
@@ -0,0 +1,18 @@+module Test.MockCat.Readme.QuickStartDemoSpec (spec) where++import Test.Hspec+import Test.MockCat++spec :: Spec+spec = do+ it "Quick Start Demo" $ do+ withMockIO $ do+ -- 1. モックを作成 ("Hello" を受け取ったら 42 を返す)+ -- 同時に「1回呼ばれるはずだ」と期待を宣言+ f <- mock ("Hello" ~> (42 :: Int))+ `expects` called once++ -- 2. f "Hello" を呼び出し、42 を得る+ f "Hello" `shouldBe` 42++ -- 3. スコープを抜ける際、宣言した期待に対する検証が自動で行われる
+ test/Test/MockCat/Readme/ReadmeSpec.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE BlockArguments #-}+module Test.MockCat.Readme.ReadmeSpec (spec) where++import Test.Hspec+import qualified Test.MockCat.Readme.QuickStartDemoSpec as QuickStartDemo+import qualified Test.MockCat.Readme.WithMockSpec as WithMock+import qualified Test.MockCat.Readme.WithMockIOSpec as WithMockIO+import qualified Test.MockCat.Readme.MakeMockSpec as MakeMock+import qualified Test.MockCat.Readme.MakeAutoLiftMockSpec as MakeAutoLiftMock+import qualified Test.MockCat.Readme.ShouldBeCalledSpec as ShouldBeCalled+import qualified Test.MockCat.Readme.MatcherSpec as Matcher+import Control.Monad (when)++spec :: Bool -> Spec+spec strictWorks = do+ describe "Readme Tests" do+ QuickStartDemo.spec+ WithMock.spec+ WithMockIO.spec+ MakeMock.spec+ MakeAutoLiftMock.spec+ Matcher.spec+ when strictWorks $ do+ ShouldBeCalled.spec
+ test/Test/MockCat/Readme/ShouldBeCalledSpec.hs view
@@ -0,0 +1,16 @@+module Test.MockCat.Readme.ShouldBeCalledSpec (spec) where++import Test.Hspec+import Test.MockCat++spec :: Spec+spec = do+ it "Function Mocking" $ do+ -- "Hello" に対して 1 を返す関数を定義 (expects は書かない)+ f <- mock ("Hello" ~> True)+ + -- 実行+ f "Hello" `shouldBe` True++ -- 事後検証 (shouldBeCalled)+ f `shouldBeCalled` "Hello"
+ test/Test/MockCat/Readme/WithMockIOSpec.hs view
@@ -0,0 +1,15 @@+module Test.MockCat.Readme.WithMockIOSpec (spec) where ++import Test.Hspec+import Test.MockCat++spec :: Spec+spec = do+ it "User Guide (withMockIO)" $ do+ withMockIO $ do+ f <- mock ("Hello" ~> True)+ `expects` called once++ let result = f "Hello"++ result `shouldBe` True
+ test/Test/MockCat/Readme/WithMockSpec.hs view
@@ -0,0 +1,19 @@+module Test.MockCat.Readme.WithMockSpec (spec) where ++import Test.Hspec+import Test.MockCat+import Control.Monad.IO.Class (MonadIO(liftIO))++spec :: Spec+spec = do+ it "User Guide (withMock)" $ do+ withMock $ do+ -- "Hello" に対して True を返すモックを定義+ f <- mock ("Hello" ~> True)+ `expects` called once++ -- 実行+ let result = f "Hello"++ liftIO $ result `shouldBe` True+
+ test/Test/MockCat/TestHelper.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Test.MockCat.TestHelper (checkStrictVerificationWorks) where++import Test.MockCat+import Test.Hspec (shouldBe)+import Control.Exception (try, ErrorCall)++-- | Checks if Strict Verification is working (i.e. not interfered by HPC/Coverage).+-- Returns True if verification succeeds.+-- Returns False if verification fails (indicating HPC is likely enabled).+checkStrictVerificationWorks :: IO Bool+checkStrictVerificationWorks = do+ result <- try $ do+ m <- mock ("ping" ~> "pong")+ m "ping" `shouldBe` "pong"+ m `shouldBeCalled` "ping"+ + case result of+ Right _ -> pure True+ Left (_ :: ErrorCall) -> pure False
test/Test/MockCat/WithMockIOSpec.hs view
@@ -15,42 +15,39 @@ describe "withMockIO" $ do it "can run IO actions directly without liftIO" $ do withMockIO $ do- f <- mock $ "hello" ~> "world"+ f <- mock ("hello" ~> "world") `expects` called once f "hello" `shouldBe` "world"- f `shouldBeCalled` "hello" it "cleans up context even if an exception occurs" $ do -- Raise an exception in the first test void $ try @ErrorCall $ withMockIO $ do- void $ mock $ "a" ~> "b"+ _ <- mock ("a" ~> "b") `expects` called once error "force fail" -- Verify that no remnants (like expectations) from the previous test remain in the second test withMockIO $ do- f <- mock $ "x" ~> "y"+ f <- mock ("x" ~> "y") `expects` called once f "x" `shouldBe` "y"- -- Verify that the expectation "a" ~> "b" from the previous test is not verified here+ -- Verify that the expectation "a" ~> "b" from the previous test is not verified here (implicit) it "supports nested withMockIO" $ do withMockIO $ do- f1 <- mock $ "outer" ~> "ok"+ f1 <- mock ("outer" ~> "ok") `expects` called once withMockIO $ do- f2 <- mock $ "inner" ~> "ok"+ f2 <- mock ("inner" ~> "ok") `expects` called once f2 "inner" `shouldBe` "ok"- f2 `shouldBeCalled` "inner" f1 "outer" `shouldBe` "ok"- f1 `shouldBeCalled` "outer" it "isolates context between parent and child threads" $ do mvar <- newEmptyMVar withMockIO $ do- f <- mock $ "parent" ~> "ok"+ f <- mock ("parent" ~> "ok") `expects` called once void $ forkIO $ do+ -- This fails because child thread cannot find the parent's mock context res <- try @SomeException (mock ("child" ~> "ok") `expects` called once) putMVar mvar res f "parent" `shouldBe` "ok"- f `shouldBeCalled` "parent" childRes <- takeMVar mvar case childRes of