[Haskell] mockcatが部分的なモックとIOアクションのスタブ関数を作れるようになりました!
私はここのところコツコツとモックライブラリmockcatの開発を続けてきたのですが、先日のリリースで、部分的なモックとIOアクションのスタブを作れるようになったので紹介します。
これまでの記事
部分的なモック
バージョン0.4.0から型クラスの一部の関数のみをスタブ関数にすることができるようになりました。
例えば型クラスApiClient
と、その型クラスを利用するfetchData
という関数があるとします。
class Monad m => ApiClient m where
makeRequest :: String -> m ByteString
parseResponse :: ByteString -> m (Maybe Int)
fetchData :: ApiClient m => String -> m (Maybe Int)
fetchData url = do
response <- makeRequest url
parseResponse response
この例では一部本物の関数を使いたいので、IO
インスタンスを用意します。
instance ApiClient IO where
makeRequest url = do
request <- parseRequest url
response <- httpLBS request
pure $ getResponseBody response
parseResponse response =
case decode response of
Just (ApiResponse obj) -> pure (Just obj)
Nothing -> pure Nothing
newtype ApiResponse = ApiResponse { value :: Int }
deriving (Generic, Show)
instance FromJSON ApiResponse
makeRequest
関数は、外部へHTTP通信を行うため、テストではモックに差し替えたいとします。
一方parseResponse
は外部への依存がないため、実関数でテストしたいです。
つまり部分的にモックにしたいわけです。
こういうときは、makePartialMock
関数を使います。
makePartialMock [t|ApiClient|]
この関数を使うと、スタブ関数を用意しなかった関数は本物の関数になります。
使用例を見てみましょう。
spec :: Spec
spec = do
it "Fetch data with mocked response" do
result <- runMockT do
_makeRequest $ "https://example.com" |> BS.pack "{\"value\": 42}"
fetchData "https://example.com"
result `shouldBe` Just 42
it "Handle invalid response" do
result <- runMockT do
_makeRequest $ "https://example.com" |> BS.pack "invalid response"
fetchData "https://example.com"
result `shouldBe` Nothing
2ケースのテストがありますが、どちらもmakeRequest
のスタブ関数だけが用意されています。
そのためparseResponse
は本物の関数が使われます。
テストを実行する際のコンテキストはIO
のため、IO
インスタンスのparseResponse
が使われます。
このように、テストしづらい部分だけをスタブ関数にしつつ、目的の関数のテストが行えるわけです。
テストの全体はこちらです。
IOアクションのスタブ
バージョン0.5.0から、IO a
型を返すスタブ関数に限り、適用する度に異なる値を返すようなスタブ関数が作れるようになりました!
ちなみに定数的に同じ値を返すことは以前のバージョンからできます。
IO a
型を返す関数とは次のようなシグニチャの関数です。
returnIO :: IO a
returnIO = ...
実例を示します。
次のような型クラスTeletype
と、関数echo
があるとします。
class Monad m => Teletype m where
readTTY :: m String
writeTTY :: String -> m ()
echo :: Teletype m => m ()
echo = do
i <- readTTY
case i of
"" -> pure ()
_ -> writeTTY i >> echo
関数echo
は、readTTY
の結果次第で処理が分岐しています。
空文字が返されたら()
を返し、そうでなければwriteTTY
を適用した後再帰します。
そのため最低限空文字が返された場合とそうでない場合の2パターンのテストを書きたいでしょう。
空文字を返す場合のテストは問題なく書けそうなのがわかると思いますが、空文字以外を返す場合のテストはそう簡単ではありません。
空文字以外が返された場合は再帰しますが、どこかで空文字以外を返さなければ無限ループしてしまうからです。
つまり適用する度に異なる値を返せる必要があります。
しかも引数がない関数でそれを実現できなければなりません。
それができることを示しましょう。
まず、次のようにオプションを付けてモックを生成します。
implicitMonadicReturn = False
は明示的にモナディックな値を返せるようにするオプションです。
makeMockWithOptions [t|Teletype|] options { implicitMonadicReturn = False }
テストは次のようになります。
2つ目のケースの_readTTY
で、1回目は"a"
を返し、2回目以降は空文字を返すスタブ関数が用意されています。
spec :: Spec
spec = doい
it "The process ends when an empty string is returned." do
result <- runMockT do
_readTTY $ pure @IO ""
echo
result `shouldBe` ()
it "If a non-empty character is returned, it is recursed." do
result <- runMockT do
_readTTY $ do
onCase $ pure @IO "a"
onCase $ pure @IO ""
_writeTTY $ "a" |> pure @IO ()
echo
result `shouldBe` ()
テストの全体はこちらです。
[補足]
デフォルトでは_readTTY ""
のように通常の値を返した場合、mockcatが自動的にpure
で包んで返すようになっていますが、今回の例の場合はIO
であることを明示したいため、モックを作る際implicitMonadicReturn = False
としています。
Polysemy
版のTeletype
)
おまけ(Polysemy
(拡張可能作用)版のTeletype
のテストもmockcatで実現できます。
まずデータ型と関数を定義します。
data Teletype m a where
ReadTTY :: Teletype m String
WriteTTY :: String -> Teletype m ()
makeSem ''Teletype
echo :: Member Teletype r => Sem r ()
echo = do
i <- readTTY
case i of
"" -> pure ()
_ -> writeTTY i >> echo
これに対してテストは次のようになります。
spec :: Spec
spec = do
it "Teletype" do
readTTYStubFn <- createStubFn do
onCase $ pure @IO "output"
onCase $ pure @IO ""
writeTTYMock <- createMock $ "output" |> pure @IO ()
let runEcho = interpret $ \case
ReadTTY -> embed readTTYStubFn
WriteTTY text -> embed $ stubFn writeTTYMock text
result <- (runM . runEcho) echo
result `shouldBe` ()
writeTTYMock `shouldApplyTo` "output"
interpret
でハンドラを作っているのですが、そこにスタブ関数を噛ませてやるだけです。
コード全体はここにあります。
おわりに
モックライブラリに開発にあたり、しばらくTemplate Haskellと戯れていたので、次はTemplete Haskellの記事を書いてもいいかもなぁと思いました。
もしくは、Elm, PureScript, Haskellそれぞれをやってみての所感とか。
なーんか思いついたらまた書きます。
ではまた。
Discussion