反応が良かったツイート
テストを無名関数で書くと楽しいよという旨のツイートの反応が良かった。ただ言葉だけだと正確に情報が伝わらないので実際にどうやっているのかをコードに落としてみた
テストケースを複数書くときは無名関数が凄く便利。わざわざ外部に関数を定義したくない・するまでもないけど、3回ぐらいは同じ処理させたい時に関数内の無名関数ならスコープも切れるので良い。この考え方も間違いなくElixirからの輸入品やおね
— OKB (@sing_mascle69) 2020年1月13日
せっかくなのでgolang
とElixir
で書いてみた
実際に無名関数を使ってテストを書いてみる
リストに含まれている値を集計し、登場回数をカウントしてmap
にして返す
golang編
動作の確認
func CountAggregeter(lst []string) map[string]int { // 集計用のmap aggregater := make(map[string]int, 0) for _, val := range lst { if _, ok := aggregater[val]; ok { aggregater[val] += 1 } else { aggregater[val] = 1 } } return aggregater }
// 実行結果 package main import "fmt" func main(){ // Your code here! lst := []string{"AAA", "BBB", "AAA", "CCC", "DDD", "BBB", "EEE"} res := CountAggregeter(lst) fmt.Println(res) } // map[AAA:2 BBB:2 CCC:1 DDD:1 EEE:1]
こちらからも確認可能です
play.golang.org
プロジェクト化
$GOPATH
以下にディレクトリを作成してファイルを用意する(今回はテストだけを試すのでmain.go
はなし)。テストのパッケージはgolang
に標準組み込みのtesting
を使用する。業務で3ヶ月使っているがシンプルなテストをする上でパフォーマンスは十分で覚えることもほとんどない。
ちなみに$GOPATH
は簡単に確認することが出来る
$ echo $GOPATH
/Users/okb/go
/nameless_func_test |── /utils |── list.go |── list_test.go
下記に先ほど作成したリストの値をカウントする関数を記述する
./nameless_func_test/utils/list.go
package utils // リストを受け取り、値の登場回数をカウント func CountAggregeter(lst []string) map[string]int { // 集計用のmap aggregater := make(map[string]int, 0) for _, val := range lst { // 既出であれば+1 if _, ok := aggregater[val]; ok { aggregater[val] += 1 } else { // 初登場であれば1を格納 aggregater[val] = 1 } } return aggregater }
testing
の仕様に従って、_test.go
をテストを行いたいファイルの語尾に付ける。今回はlist.go
に対してテストを行うのでlist_test.go
を作成。そしてお待たせ。ここで無名関数が登場する
./nameless_func_test/utils/list_test.go
// Testという名前をテストを行いたい関数の頭に付けて関数を作成 func TestCountAggregeter(t *testing.T) { // 何度も同じ処理を行うため無名関数化 debuger := func(lst []string, expectMap map[string]int) error { // 集計処理を実行 res := CountAggregeter(lst) // 期待する値を用いてloop処理 for key, val := range expectMap { // 値が集計結果に含まれているかどうか if cnt, ok := res[key]; ok { // カウント数が一致しないのであればerror if val != cnt { return errors.New("[Error] カウント数が一致しません") } } else { // そもそも結果に含まれていなければerror return errors.New("[Error] 想定値か集計値が間違っています") } } return nil } }
これがテストに使用する無名関数。引数にCountAggregeter
に渡すリストと、期待する結果(assertテストとほとんど同じ)を与えて、一致するかをrange
を使って確認するというシンプルな仕上がり。退屈なテストの記述作業が少しは面白くなるはず。
あとはこの無名関数を使ってテストをゴリゴリ書くだけ(ちなみにtesting
ではエラー終了をさせる時にt.Error()
とする)
./nameless_func_test/utils/list_test.go
func TestCountAggregeter(t *testing.T) { // シンプルなケース if err := debuger([]string{"A", "B", "C", "D"}, map[string]int{"A": 1, "B": 1, "C": 1, "D": 1}); err != nil { t.Error(err) } // 重複した値がうまくカウントされているか if err := debuger([]string{"A", "B", "A", "C"}, map[string]int{"A": 2, "B": 1, "C": 1}); err != nil { t.Error(err) } // リストが空の場合 if err := debuger([]string{}, map[string]int{}); err != nil { t.Error(err) } // 複数の値が重複する場合 if err := debuger( []string{"A", "B", "A", "C", "D", "C", "E", "F", "D", "F", "G"}, map[string]int{"A": 2, "B": 1, "C": 2, "D": 2, "E": 1, "F": 2, "G": 1}, ); err != nil { t.Error(err) } // 重複の発生が3回以上 if err := debuger([]string{"A", "A", "A"}, map[string]int{"A": 3}); err != nil { t.Error(err) } }
テストの実行と結果の確認。いいね
$ pwd
/Users/okb/go/src/nameless_func_test$ go test -v nameless_func_test/utils
=== RUN TestCountAggregeter
--- PASS: TestCountAggregeter (0.00s)
PASS
ok nameless_func_test/utils 0.007s
Elixir編
実はElixir
でテストを扱うのは初めて。今まではやってもdoctest
でしか記述をしたことが無かった。まずはCountAggregeter
と同様の機能を持つ関数の実装を行なった
defmodule Aggregater do def counter(lst) do Enum.reduce(lst, %{}, fn val, acc -> if Map.has_key?(acc, val) do Map.put(acc, val, Map.get(acc, val)+1) else Map.put(acc, val, 1) end end) end end Aggregater.counter(["AAA", "BBB", "AAA", "CCC", "DDD", "BBB", "EEE"]) |> IO.inspect
実行結果
> $ iex list.ex Erlang/OTP 22 [erts-10.6.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace] %{"AAA" => 2, "BBB" => 2, "CCC" => 1, "DDD" => 1, "EEE" => 1} Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
プロジェクト化 & テスト書く
Elixir
でのテストの記述に関してはmodule
レベルで扱うのは初めてなので以下の記事を参考にテストを書いてみる
ExUnit
を扱いたいのでmix
の新規プロジェクトを立ち上げる
$ mix new aggregater
実行後色々と生成される。今回、扱うのはlib/aggregater.ex
とtest/aggregater_test.ex
の2ファイルのみ。テストとして実行するファイルには.exs
形式を用いる必要がある模様。
まずはlib/aggregater.ex
に先ほど実装した関数を記述
./lib/aggregater.ex
defmodule Aggregater do def counter(lst) do Enum.reduce(lst, %{}, fn val, acc -> if Map.has_key?(acc, val) do Map.put(acc, val, Map.get(acc, val)+1) else Map.put(acc, val, 1) end end) end end
続いてテストのための無名関数を記述していく
./test/aggregater.ex
defmodule AggregaterTest do use ExUnit.Case doctest Aggregater # テストの命名 test "test for Aggregater.counter" do # lst -> []string # expects -> map[string]int # 無名関数の作成 debuger = fn lst, expects -> res = Aggregater.counter(lst) Enum.each(expects, fn {key, val} -> if Map.has_key?(res, key) do if val != Map.get(res, key) do IO.puts("#{val} == #{Map.get(res, key)}") # 想定外の状態が発生した場合に即return raise :error else :ok end else raise :error end end) end end end
無名関数を使って複数のテストを書いてみる
defmodule AggregaterTest do use ExUnit.Case doctest Aggregater test "test for Aggregater.counter" do # シンプルなケース assert debuger.(["A", "B", "C", "D"], %{"A" => 1, "B" => 1, "C" => 1, "D" => 1}) == :ok # 重複した値がうまくカウントされているか assert debuger.(["A", "B", "A", "C"], %{"A" => 2, "B" => 1, "C" => 1}) == :ok # リストが空の場合 assert debuger.([], %{}) == :ok # 複数の値が重複する場合 assert debuger.(["A", "B", "A", "C", "D", "C", "E", "F", "D", "F", "G"], %{"A" => 2, "B" => 1, "C" => 2, "D" => 2, "E" => 1, "F" => 2, "G" => 1}) == :ok # 重複の発生が3回以上 assert debuger.(["A", "A", "A"], %{"A" => 3}) == :ok end end
実行してみる
$ mix test Finished in 0.06 seconds
1 test, 0 failures
Elixir
で無事に無名関数を使ってテストを実行することが出来た。mix
で作成したプロジェクトであればゴリゴリとテストが書いていけそうだ。人によるが自分にとっては退屈なテストケースの記述処理が無名関数を使うことで楽しく効率化することが出来る。ご参考に。全体のコードはこちらから
github.com
追記
いつもお世話になっているfukuoka.exさんの古賀さんより、一筆頂きました
なるほど〜、その無名関数自体の動きが正しいかどうか、どう担保するかがキモですねぇ🤔
— 古賀 祥造 (@koga1020_) 2020年1月19日
ブログに上がってるようにdebugをしやすくするのが目的ならば、
- 1assert文で「test do」のブロックを作り、メッセージを分かるように工夫する
- describeでまとめる
という方針で僕なら書きますね。
サンプルコードまで書いて頂きました。確かにElixir
でテストを書くのであればブロックを分けてシンプルなassert
を一文記述する方式の方がどこでErrorになるのかも一目瞭然でクリーンになる。さらにPowerAssert
というライブラリがかなりいい感じですね
gist.github.com
PowerAssert
goでtesting使ってテストをゴリゴリ書こうとすると、Test + 関数名で命名が枯渇しがちでつらいという状態があってこの記述方法を採用しているのですが、test "test case" doでパターンを列挙できるElixirでは各実行ブロックがassertのみの記述担っている方がクリーンで分かりやすいですね。
— OKB (@sing_mascle69) 2020年1月19日
こういう反応がもらえるのが何よりありがたいし、自分のレベルアップが出来る。ありがとうございます🙇♂️