🐱

PureScriptのmockライブラリを作りました

2023/05/04に公開

はじめに

mockライブラリ pmock というものを作ったので紹介しようと思います。
https://github.com/pujoheadsoft/purescript-pmock
使い方は、githubのREADEMEに詳細に書いているので軽く済ませて、作った動機とか悩んだところも書いてみようかなと思います。

使い方

githubのREADMEと重複するところはありますが、READMEよりは説明的に書いてみようと思います。

mockされた関数

例えば次のようなものが定義されているとします。

type Article = {title :: String, body :: String}

findArticle :: String -> Int -> Article
findArticle = undefined

Articleはニュースを表すtypeです。
findArticleは、ニュースのタイトルとニュースが公開された年を指定すると条件に合致するニュースを一つだけ返すという関数です。
このfindArticleのmock関数を次のように書くことで作ることができます。

module Main where

import Prelude

import Effect (Effect)
import Effect.Console (logShow)
import Test.PMock (mockFun, (:>))

type Article = {title :: String, body :: String}

main :: Effect Unit
main = do
  let
    -- mock関数を生成
    findArticle :: String -> Int -> Article
    findArticle = mockFun $ "Title" :> 2023 :> {title: "ArticleTitle", body: "ArticleBody"}
  logShow $ findArticle "Title" 2023 -- { body: "ArticleBody", title: "ArticleTitle" }

mockFunに演算子:>で連結された値を渡すことでmock関数を作れます。
演算子:>は、演算子->を意識していますが使う分には何も考えず、引数や戻り値を指定するだけです。
これはいわゆるstub的な使い方ですね。

検証

mockFunではmock関数を作ることができましたが、その関数が期待する引数で呼び出されたこと、あるいは特定の回数呼び出されたことを検証したい場合は、verifyおよびverifyCountを使えます。
例えば「特定の条件を満たすときは呼び出されない」ということを確認する場合に使えるでしょう。
以下はverifyの例です。

import Prelude

import Test.PMock (fun, mock, verify, (:>))
import Test.Spec (Spec, it)
import Test.Spec.Assertions (shouldEqual)

spec :: Spec Unit
spec = do
  it "verify example" do
    let
      m = mock $ "Title" :> 2023 :> false

    -- 関数を取り出して実行 & shouldEqual で検証
    fun m "Title" 2023 `shouldEqual` false

    -- 指定した引数で呼び出されたか検証
    verify m $ "Title" :> 2023

mockFunのときとは異なり、まずmock関数を呼び出しています。
この関数はMock型の値を返します。
funMock型を渡すことでmock関数を取得でき、verifyで検証が行なえます。

mockFunで作ったmock関数は、期待した引数で呼び出されなかった場合はエラーを投げることでテストを失敗させる(≒検証機能も兼ねている)ので、取得系の処理はmockFunで十分かもしれません。
しかし前述のように「呼び出されなかったこと」を検証する場合やはりverifyCountは必要でしょう。

import Prelude

import Test.PMock (mock, verifyCount, (:>))
import Test.Spec (Spec, it)

spec :: Spec Unit
spec = do
  it "verify count example" do
    let
      m = mock $ "Title" :> 2023 :> false

    -- 呼び出し回数の検証(呼んでいないので成功する)
    verifyCount m 0 $ "Title" :> 2023

引数によって、返す値を変えるmock

引数によって返す値を変えたい、そんなシチュエーションがあると思います。
例えば、繰り返し処理を行う関数に渡す関数をmockにする場合がそうですね。
いつも使うわけではないけど、無いとなったとき困るやつです。
pmockの場合は、次のようにmockに配列を渡すだけで実現できます。

import Prelude

import Test.PMock (fun, mock, (:>))
import Test.Spec (Spec, it)
import Test.Spec.Assertions (shouldEqual)

spec :: Spec Unit
spec = do
  it "multi mock example" do
    let
      m = mock $ [
        "Aja" :> 1977,
        "Gaucho" :> 1980,
        "The Royal Scam" :> 1976
      ]

    fun m "Aja" `shouldEqual` 1977
    fun m "Gaucho" `shouldEqual` 1980
    fun m "The Royal Scam" `shouldEqual` 1976

    verify m "Aja"
    verify m "Gaucho"
    verify m "The Royal Scam"

その他

組み込み型のMatcherであるanyとか、自分で作るCustomeMatcherとか他にも機能はあるのですが、そちらはgithubの日本語の方を参照していただけたらと思います。

もうちょっと実用的な例は?

上記の例は、自明なことを検証しており、実用上どう使われるかイメージしづらい部分があるかと思います。
そこで、もうちょっと実用的な例のプロジェクトを作ることにしました。
できたらまた記事を書こうと思います。

作った動機

単純にPureScriptでTDDやりたかったからです。
で、mockを使いたかった。
これまで色々な言語を触ってきましたが、大体どの言語にもmockライブラリはあったので、PureScriptでもあるだろうとググってみると、あるにはあるのだけれど自分がやりたいことを実現できるmockライブラリがなかったんですね。
これは言語的にテストを書く上で皆あまり必要としていないからなのかな。
しかし自分は必要なんだよなぁ。
んー、まぁ簡単なmock的関数なら自前ですぐ作れそうだし、それで十分かも。
ということで最初はライブラリにせずこんな感じでテストを書いていました(説明において詳細はどうでもいいので、真面目に読んでいただかなくて大丈夫です👌)。

let
  port = {
    findByTitle: \_ -> pure { title: "新しいタイトル" }
  } :: ArticlePortType (StateT State Aff)
  presenter =  {
    update: \_ -> modify_ (\_ -> {article: {title: "新しいタイトル"}})
  } :: ArticlePresenterType (StateT State Aff)

result <- findArticleByTitle "Dummy" port presenter
  flip runStateT {article: {title: "古いタイトル"}}
  <#> snd

-- 書き換えた結果を使って検証
result `shouldEqual` {article: {title: "新しいタイトル"}}

これは findArticleByType という関数のテストで、ざっくりいうと指定したタイトルを持つArticleを取得してupdate関数によりstateを更新するということをテストしています。
ただ、いま内容はどうでもよくて、findByTitleupdate関数が固定値を返しているところに着目していただきたいです。
これは動きます。動く。
が、引数をチェックしていないことがどうしても気になりました。
本当に期待した通り動いているかテストとして、これだと十分ではないなぁと自分は感じました。
それに毎回こういうのを作っていくのも面倒だなぁというのと、これだと本質的にテストしたい部分がぼやけるなぁと。

ということで真面目に作るか、と作り始めました。
最初は学習用の小さなプロジェクトの中でしか使う予定がなかったので、その中で試行錯誤(というか四苦八苦?七転八起?)していたのですが、作ってるうちに段々機能が揃ってきたので、他のプロジェクトからでも使えるように別のプロジェクトにして、更に公開することにしました。

ちなみに上記のテストはこんな感じで書けるようになりました。
verifyで引数を検証できています😀

let
  findByTitleMock = mock $ "古いタイトル" :> (pure { title: "新しいtitle" } :: StateT State Aff Article)
  port = { findByTitle: fun findByTitleMock } :: ArticlePortType (StateT State Aff)

  updateMock = mock $ "新しいtitle" :> (pure unit :: StateT State Aff Unit)
  presenter =  { update: fun updateMock } :: ArticlePresenterType (StateT State Aff)

-- 結果は不要
_ <- findArticleByTitle "古いタイトル" port presenter
  # flip runStateT {article: {title: "Dummy"}} 

-- 期待する引数で呼ばれたか検証
verify findByTitleMock "古いタイトル"
verify updateMock "新しいtitle"

※がっつりと型アノテーションが書かれているのは、ArticlePortType, ArticlePresenterType, findArticleByTitleの定義がこのようになっており、全称量化された型mを扱っているからです。

type ArticlePortType m = {
  findByTitle :: String -> m Article
}

type ArticlePresenterType m = {
  update :: String -> m Unit
}

findArticleByTitle
  :: forall m
   . MonadState State m 
   => String
   -> ArticlePortType m
   -> ArticlePresenterType m
   -> m Unit

悩んだところ

インタフェース面

いわゆるstubとverifyをできるようにしたいというのは当初からあったのですが、書き味というかどんな風に書けるといいのかという所ではかなり悩みました。
ちなみに最初は

m = mock $ whenCalledWith (1 : 2) `returns` 100

って感じで文章的に読めるようなのがいいかなと思っていました。
しかしあるときふと「自分がこれまで使ってきた他の言語のmockライブラリの表現に引っ張られすぎてやしないか」という考えが頭をよぎったのです。
そしてPureScriptでは関数の引数は常に一つだったなと思い出し、ならばいっそ -> 演算子のように演算子で繋げられるようにした方が関数の定義と類似性があってよいのではと、そう考えて今の形に至りました。

こういったところは、自分が一人で使う分には割りと何でもいいけど、オープンソースとして公開するとなると悩ましい部分ですね。
色々な表現方法があるとなると尚更悩みます。

実装面

実装面では、そもそもやりたいことを実現するのにどうしたらいいのかというところで大分悩みました。
当初はmock関数自体を作って返す際、入力が可変長引数で、出力が可変長引数の関数となるような関数を「一つの定義」でどうにかしようとしていました。
入力の型は不定かつ可変長であるため、可変長引数関数の定義の仕方を調べたり、色々な型の値を持てるHeterogeneous Listのことを調べたりして実装を試みていたのですが、どうにもうまくいかない。入力はOKだったのですが、入力に応じて出力である関数の型を動的に変えることができなかったのです。
ならばいっそmock1, mock2みたいに別の関数にしてみてはどうか?
そういうことも考えましたが、使う側からしたらこれは嫌だなぁ、関数は一つにしたいよなぁと却下。
結局紆余曲折を経て「型クラスを使えばいいじゃん」と考え、入力と出力のパターン分instanceを定義する形に落ち着きました。
極力DRYにするために共通化は行いましたが、insntanceの定義部分はどうしようもなく(MultiMockの分も増えた)、冗長さは排除しきれませんでした。
型レベル計算とかでどうにかできないか、他に方法はないか考えたりもしましたが、いつまでもあるかわからない理想郷を探し続けるより、まず使えることが大事と考えて、一旦切り上げることにし、今に至ります。
使う側からしたらあまり関心のないところかもしれませんが、作る側としては綺麗に書きたいわけでして、そういうところも悩みどころでした。趣味としてのプログラミングだと無限に拘れてしまうので。

最後に

PureScriptでテストを書いている皆様、よかったら使ってみてください!
もう少し使用イメージを明確にするために、別の記事を書く予定ですので、よかったらそちらも御覧ください。
記事の内容は『PureScriptでClean Architecture + TDD』を予定しています。

書きました!
https://zenn.dev/funnycat/articles/3f853ca6d75271

Discussion