mockcat 1.4.1.0 → 1.4.1.1
raw patch · 6 files changed
+189/−40 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +4/−0
- README-ja.md +90/−19
- README.md +90/−20
- mockcat.cabal +1/−1
- test/Test/MockCat/ExampleSpec.hs +2/−0
- test/Test/MockCat/WithMockSpec.hs +2/−0
CHANGELOG.md view
@@ -6,6 +6,10 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## [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).+ ## [1.4.1.0] - 2026-01-12 ### Improved - **Mock Context Isolation**: Redesigned mock tracking to use local context containers within `withMock` and `runMockT`. This significantly reduces reliance on global registries and thread-local storage, ensuring better test isolation and predictability.
README-ja.md view
@@ -14,20 +14,33 @@ </div> **Mockcat** は、Haskell のための直感的で宣言的なモックライブラリです。-2つの検証スタイルをサポートしています。 -* **事前宣言による検証 (`expects`)**: 【推奨】 モック定義時に期待される振る舞いを宣言します。-* **事後検証 (`shouldBeCalled`)**: テスト実行後に呼び出しを検証します。+```haskell+-- スタブ: 「"a" が来たら True を返す」+let f :: String -> Bool+ f = stub ("a" ~> True) +f "a" -- => True+```++これだけです。`~>` の左に引数、右に戻り値を書くだけ。+検証は行わず、純粋な関数を返します。++**呼び出しを検証したい** なら、`mock` と `expects` を使います:+ ```haskell--- 定義と同時に検証内容を宣言 ("input" で1回呼ばれることを期待)-f <- mock ("input" ~> "output")- `expects` called once+withMockIO $ do+ f <- mock ("a" ~> True)+ `expects` called once -- 「1回だけ呼ばれるはず」と宣言 --- 実行-f "input"+ f "a" `shouldBe` True+ -- withMockIO のスコープ終了時に「1回呼ばれたか」がチェックされる ``` +> **使い分け:**+> - 値を返すだけなら `stub`(純粋、検証なし)+> - 呼び出しを検証したいなら `mock`(常に `expects` とセットで)+ --- ## 概念と用語 (Concepts & Terminology)@@ -108,24 +121,37 @@ mockcat ``` -### 最初のテスト+### 最初のテスト(検証なし)+ ```haskell import Test.Hspec import Test.MockCat spec :: Spec spec = do- it "Quick Start Demo" $ do+ it "stub demo" $ do+ let f :: String -> Int+ f = stub ("Hello" ~> 42)++ f "Hello" `shouldBe` 42+```++### 最初のテスト(検証あり)++```haskell+import Test.Hspec+import Test.MockCat++spec :: Spec+spec = do+ it "mock demo" $ do withMockIO $ do- -- 1. モックを作成 ("Hello" を受け取ったら 42 を返す)- -- 同時に「1回呼ばれるはずだ」と期待を宣言+ -- 「1回だけ呼ばれるはず」と期待を宣言 f <- mock ("Hello" ~> (42 :: Int)) `expects` called once - -- 2. f "Hello" を呼び出し、42 を得る f "Hello" `shouldBe` 42-- -- 3. スコープを抜ける際、宣言した期待に対する検証が自動で行われる+ -- withMockIO のスコープ終了時に「1回呼ばれたか」がチェックされる ``` @@ -194,9 +220,10 @@ > `expects` called once > ``` -### 2. 型クラスのモック (`makeMock`)+### 2. 型クラスを使った設計でのモック (`makeMock`) -既存の型クラスをそのままテストに持ち込みたい場合に使います。Template Haskell を使って、既存の型クラスからモックを自動生成します。+型クラスで依存を表現している設計(MTL スタイルや Capability パターン)において、+そのままテストに持ち込みたい場合に使います。Template Haskell を使って、型クラスからモックを自動生成します。 ```haskell {-# LANGUAGE TemplateHaskell #-}@@ -335,8 +362,8 @@ #### mock vs stub vs mockM の使い分け -基本的には **`mock`** だけ覚えれば十分です。-より細かい制御が必要になった場合に、他の関数を検討してください。+使う関数は、テスト対象の性質に応じて選択するとよいでしょう。+細かい違いは以下の表を参照してください。 | 関数 | 検証 (`shouldBeCalled`) | IO 依存 | 特徴 | | :--- | :---: | :---: | :--- |@@ -352,6 +379,17 @@ * `String -> Int` のような**純粋な関数**をモックする場合に使用します。 * Haskell の遅延評価を尊重し、「実際に結果が評価されたタイミング」で呼び出しを記録します。これにより、使われていない無駄な呼び出しをカウントしてしまうのを防ぎます。 +> [!IMPORTANT]+> `mock` は純粋な関数 (`a -> b`) として振る舞うため、+> **GHC の最適化(CSE / CAF 化 / full laziness)の影響を受けます**。+>+> その結果、ソースコード上では複数回書かれていても、+> コンパイル後のプログラムでは **1回しか評価されない** 場合があります。+>+> Mockcat は「ソース上の記述回数」ではなく、+> **実際に評価された回数**を記録・検証します。++ * **`mockM` (IO/モナディックな関数向け)**: * `String -> IO Int` のような、**`IO` や `ReaderT IO` などの `MonadIO` インスタンスを返す関数**をモックする場合に使用します。 * 記録処理が戻り値のアクション(`IO`)自体に組み込まれているため、高度な並列テストや強力な最適化がかかる環境下でも、非常に高い予測可能性を提供します。@@ -534,6 +572,39 @@ A. はい、xUnit Patterns 等の定義に従えば、事後検証を行う Mockcat のモックは **Test Spy** に分類されます。<br> しかし、近年の多くのライブラリ(Jest, Mockito 等)がこれらを包括して「モック」と呼称していること、および用語の乱立による混乱を避けるため、本ライブラリでは **"Mock"** という用語で統一しています。 </details>++<details>+<summary><strong>Q. テストで呼び出し回数が期待より少なくカウントされることがあります</strong></summary>++A. `mock` は純粋関数として扱われるため、GHC の最適化により評価が共有される場合があります。+これは Mockcat の仕様であり、コンパイル後の実行結果を正確に反映した挙動です。++Mockcat はランタイムにおける **「実際の評価回数」** を記録します。+そのため、最適化によって同一の式が共有された場合は、事実通り「1回」とカウントされます。++テスト目的で評価の共有を抑制したい場合は、テストファイルに以下の+**GHC プラグマ**を指定することもできます。++```haskell+{-# OPTIONS_GHC -fno-cse #-}+{-# OPTIONS_GHC -fno-full-laziness #-}+```++これは **テスト専用の設定** で、ソースコード上の呼び出し回数に近い挙動を確認したい場合に便利です。+</details>++## 実践的な例++Mockcat は特定のアーキテクチャを前提としません。+テスト基盤の中核として使うことも、既存のフレームワーク内で軽量なヘルパーとして使うこともできます。++以下は mockcat を使った実践的なテストスイートです:++- **MTL + Capability パターン**: Port ベースのアプリケーションテスト(`MockT` / `ExceptT` を使用)+ 👉 [UsecaseSpec.hs (cli-mtl)](https://github.com/pujoheadsoft/haskell-layered-examples/blob/main/cli-mtl/test/Application/UsecaseSpec.hs)++- **Polysemy エフェクト**: stub でデータを流し、mock で検証が必要な箇所だけを検証+ 👉 [UsecaseSpec.hs (cli-effect-polysemy)](https://github.com/pujoheadsoft/haskell-layered-examples/blob/main/cli-effect-polysemy/test/Application/UsecaseSpec.hs) ## ヒントとトラブルシューティング
README.md view
@@ -13,21 +13,34 @@ </div> -**Mockcat** is a lightweight, declarative mocking library for Haskell. -It supports two verification styles:+**Mockcat** is a lightweight, declarative mocking library for Haskell. -* **Declarative Verification (`expects`)**: **[Recommended]** Declaratively state the expected behavior at mock definition time.-* **Post-hoc Verification (`shouldBeCalled`)**: Verify call history after test execution.+```haskell+-- Stub: Returns True when given "a"+let f :: String -> Bool+ f = stub ("a" ~> True) +f "a" -- => True+```++That's it. Write the argument on the left of `~>`, the return value on the right.+No verification—just a pure function.++**To verify calls**, use `mock` with `expects`:+ ```haskell--- Define and Expect at the same time ("input" should be called exactly once)-f <- mock ("input" ~> "output")- `expects` called once+withMockIO $ do+ f <- mock ("a" ~> True)+ `expects` called once -- Declare "should be called exactly once" --- Execute-f "input"+ f "a" `shouldBe` True+ -- 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`)+ --- ## Concepts & Terminology@@ -108,7 +121,7 @@ mockcat ``` -### First Test (`Main.hs` / `Spec.hs`)+### First Test (No Verification) ```haskell import Test.Hspec@@ -116,17 +129,29 @@ spec :: Spec spec = do- it "Quick Start Demo" $ do+ it "stub demo" $ do+ let f :: String -> Int+ f = stub ("Hello" ~> 42)++ f "Hello" `shouldBe` 42+```++### First Test (With Verification)++```haskell+import Test.Hspec+import Test.MockCat++spec :: Spec+spec = do+ it "mock demo" $ do withMockIO $ do- -- 1. Create a mock (Return 42 when receiving "Hello")- -- Simultaneously declare "it should be called once"+ -- Declare "should be called exactly once" f <- mock ("Hello" ~> (42 :: Int)) `expects` called once - -- 2. Call f "Hello" and get 42 f "Hello" `shouldBe` 42-- -- 3. Verification happens automatically when exiting the scope+ -- Verification runs when exiting the withMockIO scope ``` @@ -195,9 +220,10 @@ > `expects` called once > ``` -### 2. Typeclass Mocking (`makeMock`)+### 2. Mocking with Typeclass-Based Designs (`makeMock`) -Useful when you want to bring existing typeclasses directly into your tests. Generates mocks from existing typeclasses using Template Haskell.+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. ```haskell {-# LANGUAGE TemplateHaskell #-}@@ -331,8 +357,8 @@ #### mock vs stub vs mockM -In most cases, **`mock`** is all you need.-Consider other functions only when you need finer control.+Choose the function based on the nature of your test target.+See the table below for details. | Function | Verification (`shouldBeCalled`) | IO Dependency | Characteristics | | :--- | :---: | :---: | :--- |@@ -348,6 +374,17 @@ * Use this when mocking **pure functions** like `String -> Int`. * It respects Haskell's lazy evaluation and records the call only when the result is actually evaluated. This prevents counting unnecessary calls that were never executed. +> [!IMPORTANT]+> Since `mock` behaves as a pure function (`a -> b`),+> **it is subject to GHC's optimizations (CSE / CAF / full laziness)**.+>+> As a result, even if an expression appears multiple times in your source code,+> **it may be evaluated only once** after compilation.+>+> Mockcat records and verifies **the actual number of evaluations**,+> not the number of times the expression appears in the source code.++ * **`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.@@ -530,6 +567,39 @@ A. Yes, according to definitions like xUnit Patterns, Mockcat's mocks which verify after execution are classified as **Test Spies**.<br> However, since many modern libraries (Jest, Mockito, etc.) group these under "Mock", and to avoid confusion from terminology proliferation, this library unifies them under the term **"Mock"**. </details>++<details>+<summary><strong>Q. Call counts are lower than expected in tests</strong></summary>++A. Since `mock` is treated as a pure function, GHC's optimizations may cause evaluations to be shared.+This is Mockcat's intended behavior—it accurately reflects the actual execution result after compilation.++Mockcat records **"the actual number of evaluations"** at runtime.+Therefore, if the same expression is shared due to optimization, it is correctly counted as "1 call".++If you want to suppress evaluation sharing for testing purposes, you can add the following+**GHC pragmas** to your test file.++```haskell+{-# OPTIONS_GHC -fno-cse #-}+{-# OPTIONS_GHC -fno-full-laziness #-}+```++This is a **test-only setting**, useful when you want to verify behavior closer to the source-level call count.+</details>++## 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.++Here are real-world test suites using mockcat:++- **MTL + Capability pattern**: Port-based application tests (using `MockT` / `ExceptT`)+ 👉 [UsecaseSpec.hs (cli-mtl)](https://github.com/pujoheadsoft/haskell-layered-examples/blob/main/cli-mtl/test/Application/UsecaseSpec.hs)++- **Polysemy effects**: Use stub for data flow, mock only where verification is needed+ 👉 [UsecaseSpec.hs (cli-effect-polysemy)](https://github.com/pujoheadsoft/haskell-layered-examples/blob/main/cli-effect-polysemy/test/Application/UsecaseSpec.hs) ## Tips and Troubleshooting
mockcat.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: mockcat-version: 1.4.1.0+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.
test/Test/MockCat/ExampleSpec.hs view
@@ -11,6 +11,8 @@ {-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-deprecations #-}+{-# OPTIONS_GHC -fno-full-laziness #-}+{-# OPTIONS_GHC -fno-cse #-} module Test.MockCat.ExampleSpec (spec) where
test/Test/MockCat/WithMockSpec.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# OPTIONS_GHC -Wno-unused-do-bind #-}+{-# OPTIONS_GHC -fno-full-laziness #-}+{-# OPTIONS_GHC -fno-cse #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TemplateHaskell #-}