🚀

AssertMatchを用いてパターンマッチによるassertをパイプで繋いで実行する

2022/09/18に公開

2022/09/17にリモートでtokyo.ex#20が開催されました。

https://beam-lang.connpass.com/event/258131/

メインセッションのなかで@ymtszwさんによる「Further leveraging pattern matches in Elixir tests!」という発表があり、Elixirでのテストのテクニックが紹介されました。

資料も公開いただいています、ありがとうございます。

https://docs.google.com/presentation/d/e/2PACX-1vRuIA2ocDafLRJUn6nWScZmOq6YwpqXba7x5RG72yzT3X7FB-JcET33QMGsBidHsAdbnVF9KYCOa00R/pub?start=false&loop=false&delayms=3000&slide=id.p

発表の中でパターンマッチによるassertionをパイプ演算子でイイ感じに繋げられるようにした「AssertMatch」なるOSSを開発されたとのことで、今回の記事ではAssertMatchをお試ししてみたいと思います。
https://github.com/siiibo/assert_match

前提知識

ExUnitのassertマクロでは == による比較だけでなく、パターンマッチによるアサーションが可能です。

https://hexdocs.pm/ex_unit/ExUnit.Assertions.html#assert/1

https://qiita.com/kentaro/items/477c92a57c8aaf694251

  test "assert by pattern match" do
    attrs = %{a: 1, b: 2}

    assert %{a: 1} = attrs
  end

左辺と右辺を逆にするとただの変数へのbindingになってしまうため注意が必要です。

  test "assert by pattern match" do
    attrs = %{a: 1, b: 2}

    # これだとテストにならない
    assert attrs = %{a: 1}
  end

最小構成でAssertMatchを試す

なんらかライブラリの挙動を試すときにはLivebookが便利です。

https://zenn.dev/koga1020/articles/c2eac1f87fbb86

以下、動作検証をしたlivebookの結果のコピペです。動かした結果がそのままmarkdownになるのは最高ですね!

Mix.install([
  {:assert_match, github: "siiibo/assert_match"}
])

* Getting assert_match (https://github.com/siiibo/assert_match.git)
remote: Enumerating objects: 85, done.        
remote: Counting objects: 100% (85/85), done.        
remote: Compressing objects: 100% (44/44), done.        
remote: Total 85 (delta 31), reused 68 (delta 21), pack-reused 0        
origin/HEAD set to main
==> assert_match
Compiling 1 file (.ex)
Generated assert_match app

:ok

バージョン

System.version()

"1.14.0"

テストコード

試しに構造体とリストのパターンマッチをassert_matchを利用して書いてみます。

利用方法はassert_match本体のテストコードを見ると分かりやすいです。

  • import AssertMatch を実行
  • あとは assert_match/2 を使うだけ

という使い方のようです。

ExUnit.start()

defmodule User do
  defstruct [:name, :age]

  def build(attrs) do
    %User{name: attrs.name, age: attrs.age}
  end
end

defmodule SampleTest do
  use ExUnit.Case
  import AssertMatch

  test "構造体のマッチ" do
    attrs = %{name: "foo", age: 20}

    # pipeで書くとこんな感じ
    # IO.inspectなどと同様、pipeの入力をそのまま
    # 出力するような設計にされており、pipeで繋げられる!
    attrs
    |> User.build()
    |> assert_match(%{name: "foo"})
    |> assert_match(%{age: 20})
  end

  test "リストのマッチ" do
    1..10//2
    |> Enum.to_list()
    |> assert_match([1, 3, 5 | _])
  end
end

ExUnit.run()

..
Finished in 0.00 seconds (0.00s async, 0.00s sync)
2 tests, 0 failures

Randomized with seed 718926

%{excluded: 0, failures: 0, skipped: 0, total: 2}

無事にテストが通っています🎉

まとめ

簡単ですがassert_matchについて紹介しました。こういったDSLはチームによって入れる・入れない分かれるところではありますが、これぐらい小さなツールであればそこまで学習コストなく使えるのではと思います。

今回の例だとミニマムすぎてありがたみが薄い例になってしまいましたが、@ymtszwさんの発表にもあったようにPlug.Conn周りのリクエストのテストなど、パイプで書けると中間の変数定義がなくなってスッキリするケースは多々あると思います。

おわりに

tokyo.exでは今後も定期的にイベントが開催される予定とのことですので、ぜひconnpassの登録なり、slackのworkspace->#tokyo-exチャンネルにjoinいただくなり、情報をウォッチしていただけたらと思います!

elixir.jp招待リンク

https://beam-lang.connpass.com/

Discussion