Declarative mocking with a single arrow ~>
[](https://hackage.haskell.org/package/mockcat)
[](http://stackage.org/lts/package/mockcat)
[](https://github.com/pujoheadsoft/mockcat/actions)
[🇺🇸 English](README.md)
**Mockcat** は、Haskell のための直感的で宣言的なモックライブラリです。
専用の演算子 **Mock Arrow (`~>`)** を使うことで、関数定義と同じような感覚で、引数と振る舞いをモックとして記述できます。
```haskell
-- 定義 (Define)
f <- mock $ "input" ~> "output"
-- 検証 (Verify)
f `shouldBeCalled` "input"
```
---
## 概念と用語 (Concepts & Terminology)
Mockcat は、「実行時に検証を行いますが、定義時に『満たすべき条件』を宣言できる」という設計を採用しています。
* **Stub (スタブ)**:
テストを進めるために値を返すだけの存在。「どう呼ばれたか」に関心を持ちません。
`stub` 関数は完全に純粋な関数を返します。
* **Mock (モック)**:
スタブの機能に加え、「期待通りに呼び出されたか」を記録・検証する存在。
`mock` 関数は、呼び出しを記録しながら値を返します。検証はテストの最後に行うことも、モック定義時に「この条件で呼ばれるはずだ」と宣言することも可能です(`expects` による宣言的検証)。
---
## Why Mockcat?
Haskell におけるモック記述を、できるだけ自然な形で行えるよう設計されています。
Mockcat は、**「アーキテクチャに依存せず、関数の "振る舞いと意図" を宣言的に記述できる」** モックライブラリです。
「型クラス (MTL) を導入しないとテストできない」
「モックのために専用のデータ型を定義しなければならない」
(例: 型クラスを増やす/Service Handle 用のレコードを別途用意する、など)
そんな制約から解放されます。既存の関数をそのままモックし、設計が固まりきっていない段階でもテストを書き進めることができます。
**Mockcat は、テストのために設計を固定するのではなく、設計を試すためにテストを書けることを目指しています。**
### Before / After
Mockcat を使うことで、テスト記述は次のようになります。
| | **Before: 手書き...** 😫 | **After: Mockcat** 🐱✨ |
| :--- | :--- | :--- |
| **定義 (Stub)**
「この引数には
この値を返したい」 | f :: String -> IO String
f arg = case arg of
"a" -> pure "b"
_ -> error "unexpected"
_単純な分岐を書くだけでも行数を消費します。_ | -- 検証不要なら stub (純粋)
let f = stub $
"a" ~> "b"
_完全に純粋な関数として振る舞います。_ |
| **検証 (Verify)**
「正しく呼ばれたか
テストしたい」 | -- 記録の仕組みから作る必要がある
ref <- newIORef []
let f arg = do
modifyIORef ref (arg:)
...
-- 検証ロジック
calls <- readIORef ref
calls \`shouldBe\` ["a"]
_※ これはよくある一例です。実際にはさらに補助コードが増えがちです。_ | -- 検証したいなら mock (内部で記録)
f <- mock $ "a" ~> "b"
-- 検証したい内容を書くだけ
f \`shouldBeCalled\` "a"
_記録は自動。
「何を検証するか」という本質に集中できます。_ |
### 主な特徴
* **Haskell ネイティブな DSL**: 冗長なデータコンストラクタや専用の記法を覚えなくても、関数定義と同じ感覚 (`引数 ~> 戻り値`) で自然に記述できます。
* **アーキテクチャ非依存**: MTL (型クラス)、Service Handle (レコード)、あるいは純粋な関数。どのような設計パターンを採用していても、最小単位で導入可能です。
* **値ではなく「条件」で検証**: 引数が `Eq` インスタンスを持っていなくても問題ありません。値の一致だけでなく、「どのような性質を満たすべきか」という条件 (Predicate) で検証できます。
* **圧倒的に親切なエラー**: テスト失敗時、どこが違うのかを「構造差分」で表示します。
```text
Expected arguments were not called.
expected: [Record { name = "Alice", age = 20 }]
but got: [Record { name = "Alice", age = 21 }]
^^
```
* **意図を導く型設計**: 型はあなたの記述を縛るものではなく、テストの意図(何を期待しているか)を自然に表現させるために存在します。
---
## クイックスタート
以下のコードをコピペすれば、今すぐ Mockcat を体験できます。
### インストール
`package.yaml`:
```yaml
dependencies:
- mockcat
```
または `.cabal`:
```cabal
build-depends:
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)
-- 2. 関数として使う
let result = f "Hello"
result `shouldBe` 42
-- 3. 呼び出されたことを検証
f `shouldBeCalled` "Hello"
```
---
### At a Glance: Matchers
| Matcher | Description | Example |
| :--- | :--- | :--- |
| **`any`** | どんな値でも許可 | `f <- mock $ any ~> True` |
| **`expect`** | 条件(述語)で検証 | `f <- mock $ expect (> 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 で解説)
### 1. 関数のモック (`mock`) - [基本]
最も基本的な使い方です。特定の引数に対して値を返す関数を作ります。
```haskell
-- "a" -> "b" -> True を返す関数
f <- mock $ "a" ~> "b" ~> True
```
**柔軟なマッチング**:
具体的な値だけでなく、条件(述語)を指定することもできます。
```haskell
-- 任意の文字列 (param any)
f <- mock $ any ~> True
-- 条件式 (expect)
f <- mock $ expect (> 5) "> 5" ~> True
```
### 2. 型クラスのモック (`makeMock`)
既存の型クラスをそのままテストに持ち込みたい場合に使います。Template Haskell を使って、既存の型クラスからモックを自動生成します。
```haskell
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeOperators #-}
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|]
-- [Auto-Lift Mode] 利便性重視のモード。
-- 純粋な値を自動的にモナド(m String など)に包んで返します。
makeAutoLiftMock [t|FileSystem|]
```
テストコード内では `runMockT` ブロックを使用します。
```haskell
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 ()
-- [Auto-Lift Mode] (makeAutoLiftMock 使用時): 値は自動的に包まれる (便利)
-- _readFile $ "config.txt" ~> "debug=true"
-- テスト対象コードの実行(モックが注入される)
myProgram "config.txt"
result `shouldBe` ()
```
### 3. 宣言的な検証 (`withMock` / `expects`)
定義と同時に期待値を記述するスタイルです。スコープを抜ける時に自動的に検証が走ります。
「定義」と「検証」を近くに書きたい場合に便利です。
```haskell
withMock $ do
-- 定義と同時に期待値(expects)を書く
f <- mock (any ~> True)
`expects` do
called once `with` "arg"
-- 実行
f "arg"
```
> [!NOTE]
> `runMockT` ブロックの中でも、同様に `expects` を使った宣言的検証が可能です。
> つまり、「モック生成」と「期待値宣言」が1つのブロック内で完結する統一された体験を提供します。
### 4. 柔軟な検証(マッチャー)
引数が `Eq` インスタンスを持っていなくても、あるいは特定の値に依存したくない場合でも、「どのような条件を満たすべきか」という**意図**で検証できます。
Mockcat は、値の一致だけでなく、関数の性質を検証するための**マッチャー**を提供しています。
#### 任意の値を許可 (`any`)
```haskell
-- どんな引数で呼ばれても True を返す
f <- mock $ any ~> True
-- 何でもいいから呼ばれたことを検証
f `shouldBeCalled` any
```
#### 条件を指定して検証 (`expect`)
任意の値ではなく、「条件(述語)」を使って検証できます。
`Eq` を持たない型(関数など)や、部分的な一致を確認したい場合に強力です。
```haskell
-- 引数が "error" で始まる場合のみ False を返す
f <- mock $ do
onCase $ expect (\s -> "error" `isPrefixOf` s) "start with error" ~> False
onCase $ any ~> True
```
### 5. 高度な機能 - [応用]
#### mock vs stub vs mockM の使い分け
基本的には **`mock`** だけ覚えれば十分です。
より細かい制御が必要になった場合に、他の関数を検討してください。
| 関数 | 検証 (`shouldBeCalled`) | IO依存 | 特徴 |
| :--- | :---: | :---: | :--- |
| **`stub`** | ❌ | なし | **純粋なスタブ**。IO に依存しません。検証不要ならこれで十分です。 |
| **`mock`** | ✅ | あり(隠蔽) | **モック**。純粋関数として振る舞いますが、内部的には IO を介して呼び出し履歴を管理します。 |
| **`mockM`** | ✅ | あり(明示) | **Monadic モック**。`MockT` や `IO` の中で使い、副作用(ロギングなど)を明示的に扱えます。 |
#### 部分モック (Partial Mock): 本物の関数と混ぜて使う
一部のメソッドだけモックに差し替え、残りは本物の実装を使いたい場合に便利です。
```haskell
-- [Strict Mode]
makePartialMock [t|FileSystem|]
-- [Auto-Lift Mode]
-- makeAutoLiftMock と同様に、Partial Mock にも Auto-Lift 版があります。
makeAutoLiftPartialMock [t|FileSystem|]
instance FileSystem IO where ... -- 本物のインスタンスも必要
test = runMockT do
_readFile $ "test" ~> pure @IO "content" -- readFile だけモック化 (Strict)
-- or
-- _readFile $ "test" ~> "content" -- (Auto-Lift)
program -- writeFile は本物の IO インスタンスが走る
```
#### IO アクションを返す (Monadic Return)
`IO` を返す関数で、呼び出しごとに副作用(結果)を変えたい場合に使います。
```haskell
f <- mock $ do
onCase $ "get" ~> pure @IO 1 -- 1回目
onCase $ "get" ~> pure @IO 2 -- 2回目
```
#### 名前付きモック
エラーメッセージに関数名を表示させたい場合は、ラベルを付けられます。
```haskell
f <- mock (label "myAPI") $ "arg" ~> True
```
---
## リファレンス & レシピ (Encyclopedia)
※ このセクションは、困ったときの辞書として使ってください。
### 検証マッチャ一覧 (`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` |
| `expect pred label` | 条件式 | `expect (>0) "positive" ~> True` |
| `expect_ pred` | ラベルなし | `expect_ (>0) ~> True` |
### 宣言的検証 DSL (`expects`)
`expects` ブロックでは、呼び出しに関する期待を宣言的に記述できます。
`expects` で使用できる構文は、`shouldBeCalled` と同じ語彙を共有しています。
| 構文 | 意味 |
| :--- | :--- |
| `called` | 呼び出しに関する期待の開始 |
| `once` | 1 回だけ呼ばれる |
| `times n` | n 回呼ばれる |
| `never` | 呼ばれない |
| `with arg` | 引数の期待値 |
| `with matcher` | マッチャを用いた引数検証 |
### よくある質問 (FAQ)
Q. 未評価の遅延評価はどう扱われますか?
A. カウントされません。Mockcat は「結果が評価された時点」で呼び出しを記録します (Honest Laziness)。これにより、不要な計算による誤検知を防ぎます。
Q. 並列テストで使えますか?
A. はい。内部で `TVar` を使用してアトミックにカウントしているため、`mapConcurrently` などで並列に呼ばれても正確に記録されます。
Q. `makeMock` が生成するコードは何ですか?
A. 指定された型クラスの `MockT m` インスタンスと、各メソッドに対応する `_メソッド名` というスタブ生成関数定義です。
Q. 厳密な定義では Spy ではないですか?
A. はい、xUnit Patterns 等の定義に従えば、事後検証を行う Mockcat のモックは **Test Spy** に分類されます。
しかし、近年の多くのライブラリ(Jest, Mockito 等)がこれらを包括して「モック」と呼称していること、および用語の乱立による混乱を避けるため、本ライブラリでは **"Mock"** という用語で統一しています。
## ヒントとトラブルシューティング
### `any` と `Prelude.any` の名前衝突
`Test.MockCat` をインポートすると、パラメータマッチャの `any` が `Prelude.any` と衝突することがあります。
その場合は `Prelude` の `any` を隠すか、修飾名を使用してください。
```haskell
import Prelude hiding (any)
-- または
import qualified Test.MockCat as MC
```
### `OverloadedStrings` 使用時の型推論エラー
`OverloadedStrings` 拡張を有効にしている場合、文字列リテラルの型が曖昧になり、エラーが発生することがあります。
その場合は明示的に型注釈を付けてください。
```haskell
mock $ ("value" :: String) ~> True
```
---
_Happy Mocking!_ 🐱