田舎で並行処理の夢を見る

試したこと、需要がないかもしれないけど細々とアウトプットしてます

【golang/Elixirのサンプル有り】何度も同じようなテストを書くのがつらいので無名関数を使って楽しくしよう

反応が良かったツイート

テストを無名関数で書くと楽しいよという旨のツイートの反応が良かった。ただ言葉だけだと正確に情報が伝わらないので実際にどうやっているのかをコードに落としてみた

せっかくなのでgolangElixirで書いてみた

実際に無名関数を使ってテストを書いてみる

リストに含まれている値を集計し、登場回数をカウントしてmapにして返す

  • go version: go1.12.4 darwin/amd64
  • Elixir version: Erlang/OTP 22 & Interactive Elixir (1.9.4)

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レベルで扱うのは初めてなので以下の記事を参考にテストを書いてみる

elixirschool.com

ExUnitを扱いたいのでmixの新規プロジェクトを立ち上げる

$ mix new aggregater

実行後色々と生成される。今回、扱うのはlib/aggregater.extest/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さんの古賀さんより、一筆頂きました

サンプルコードまで書いて頂きました。確かにElixirでテストを書くのであればブロックを分けてシンプルなassertを一文記述する方式の方がどこでErrorになるのかも一目瞭然でクリーンになる。さらにPowerAssertというライブラリがかなりいい感じですね gist.github.com

PowerAssert

t.co

こういう反応がもらえるのが何よりありがたいし、自分のレベルアップが出来る。ありがとうございます🙇‍♂️

参考文献