ElixirでProperty-Based Testing


Elixir 1.6または1.7から入るらしい[Property-Based Testing](http://elixir-lang.github.io/blog/2017/10/31/stream-data-property-based-testing-and-data-generation-for-elixir/)とは何なのかを見ていきます

## Property-Based Testingとは

以下のようにサンプルになる値をテストするいつものやり方をExample-Based Testingと呼ぶとします

```elixir
assert 1 + 1 == 2
```

Property-Based Testingは関数の性質をProperyと呼びます。例えば

```elixir
def add(x, y) do
  x + y
end
```

のような関数がある場合この`add`関数の引数を2つとってそれぞれを足してその結果を返すという性質(特性)をPropertyと呼びます


## Propertyとは

以上のことからPropertyとは一言で言えばプログラムの特性と言えます
なのでProperty-Based Testingは特性に基づいてテストすることといえます
Example-Based Testingがプログラマが期待する入力値と出力値を元にテストすることと比較して、プログラムの特性に基づいてテストするというところが違います


この特性をテストするためにProperty-Based Testingでは大量のサンプルデータを自動的に生成してテストをします
Example-Based Testingがたった一つの正しい答えに基づいてプログラムの正当性を保障するのに対して、Property-Based Testingは一度の正しさを保障するのではなく大量のランダムなデータを渡すことで**間違いを見つける**という点が主目的になっているのが大きな違いでしょう。

言い換えれば大量のランダムなデータを渡しても要求を満たしていることで帰納的に正しさを保障するテスト手法ともいえるでしょう。

## 使用例

後々Elixirのコアにmergeされる予定で現在は[StreamData](https://github.com/whatyouhide/stream_data)として別リポジトリでProperty-Based Testing用のフレームワークがElixirコアチームによって開発されています

mix.exsに依存を追加して試してみます

```elixir
{:stream_data, "~> 0.1", only: :test}
```

```elixir
use ExUnitProperties

property "length/1 is always >= 0" do
  check all list <- list_of(term()) do
    assert length(list) >= 0
  end
end
```

`check/2`macroに渡される最初の引数がジェネレータのリストで`do: `ブロックで実行されるのがテスト本体です

`check/2`の後に`all`がいきなり出てきてなんだこの未定義の変数は？それとも関数か？と面食らいますがコードを見てみると`property`macro内で`check all`を含めた引数を`quote`してASTのrepresentationを得てその表現を`check/2`macroに渡してパターンマッチングしているので問題なく実行出来ているという仕組みです
macroの入れ子になっててややこしい

`term/0`でElixirのtermをランダムで生成するジェネレータを作ります

```elixir
iex(3)> term()              
%StreamData{generator: #Function<58.22714937/2 in StreamData.tree/2>}
```

ジェネレータはStreamでEnumerable Protocolが実装してあるのでEnumで値を出力することも出来ます

```elixir
iex(4)> term() |> Enum.take(3)
[{#Reference<0.3658627003.4231266308.20877>, true, false}, <<6>>, :y3G]
```

更に`list_of/1`で`term/0`で生成したジェネレータをジェネレータに突っ込んでランダムなtermのlistを生成するジェネレータを作ります

```elixir
iex(5)> list_of(term())
%StreamData{generator: #Function<36.22714937/2 in StreamData.list_of/2>}
```

このジェネレータで生成された値が最終的に`list`に格納され`assert length(list) >= 0`でExUnitの通常のテストが実行されます

デフォルトでジェネレータが走る回数は`100`回のためこの場合100回、最大100個の値を持つリストが生成され`assert length(list) >= 0`が実行されます

ジェネレータが走る回数を制限するには`check`macroに`max_runs`オプションを指定するとcheck内のジェネレータが走る回数が制限されます
例えば以下のように`max_runs: 10`とすると最大10個のアイテムを持つリストが10回生成され10回テストされます

`check all list <- list_of(term()), max_runs: 10 do`

個別のジェネレータにもオプションがあり例えば`list_of/2`では`max_length`で生成するリストの最大長を指定出来ます。なお`max_runs`でより小さい数を指定した場合`max_runs`が優先されます

```elixir
$ mix test
..

Finished in 4.0 seconds
1 doctest, 1 property, 0 failures

Randomized with seed 561962
```

## ジェネレータ

ジェネレータが豊富に用意されているのでよく使いそうなものを羅列してそれぞれサンプルの値を`Enum.take/2`で３つ程取ってみます

### atom/1

Atomを生成する

```elixir
# 英数字
iex(17)> atom(:alphanumeric) |> Enum.take(3)
[:DN, :lR, :x_]
# alias
iex(18)> atom(:alias) |> Enum.take(3)       
[OJ, N3.I, RO.Qdv]
```

### string/1

文字列を生成する

```elixir
# ascii文字のみ
iex(252)> string(:ascii) |> Enum.take(3)                              
["", "@N", "EL"]
# 英数字のみ
iex(254)> string(:alphanumeric) |> Enum.take(3)                       
["u", "S", ""]
# Rangeで指定
iex(255)> string([?a..?z, ?A..?Z, ?0..?9], length: 10) |> Enum.take(3)
["OSRfonhf3y", "LmBZqdNsKT", "5OU6BBavps"]

```

### boolean/0

真偽値を生成する

```elixir
iex(24)> boolean() |> Enum.take(3)
[false, true, false]
```



### float/1

浮動小数点数を生成する

```elixir
iex(36)> float() |> Enum.take(3)
[0.0, 0.0, -1.125]
iex(37)> float(min: 0) |> Enum.take(3)
[0.0, 2.5, 1.5]
iex(38)> float(max: 10) |> Enum.take(3)
[10.0, 7.75, 2.25]
iex(39)> float(min: 5, max: 10) |> Enum.take(3)
[10.0, 10.0, 8.75]
```

### integer/1

整数を生成する

```elixir
iex(40)> integer() |> Enum.take(3)
[0, -2, 3]
iex(48)> integer(1..100) |> Enum.take(3)
[28, 59, 45]
```

### list_of/2

データを元にリストを生成する

```elixir
iex(85)> list_of(float()) |> Enum.take(3)            
[[], [0.5, 1.0], []]
iex(86)> list_of(float(), length: 5) |> Enum.take(3)
[
  [-0.5, 0.0, -2.0, 1.0, 0.0],
  [0.5, 1.5, 5.0, 0.5, -0.5],
  [-6.75, 5.0, 0.125, -0.75, 5.25]
]
iex(87)> list_of(float(), max_length: 3) |> Enum.take(3)
[[0.0], [], [0.625, 0.25]]
iex(88)> list_of(float(), min_length: 3) |> Enum.take(3)
[[0.0, 0.0, -0.5], [0.5, -2.5, -0.75], [0.625, 0.125, -0.75]]
```

### fixed_list/1

固定長のlistを生成する

```elixir
iex(33)> fixed_list([integer(), binary()]) |> Enum.take(3)
[[0, ""], [-1, <<231>>], [-3, "t"]]
```

### fixed_map/1

要素が固定されたmapを生成する

```elixir
iex(35)> fixed_map(%{integer: integer(), binary: binary()}) |> Enum.take(3)
[
  %{binary: "", integer: 0},
  %{binary: <<141, 177>>, integer: 0},
  %{binary: <<238, 123, 72>>, integer: 3}
]
```

### one_of/1

リストのうちから一つランダムで選ぶ

```elixir
iex(107)> one_of([true, false]) |> Enum.take(3)
[false, true, true]
```

### map_of/2

指定されたkeyとvalueからmapを生成する

```elixir
iex(143)> map_of(atom(:alphanumeric), integer()) |> Enum.take(3)
[%{N: 1}, %{}, %{_k: -2, m: -3, u5x: 1}]
```

###  nonempty/1

空のenumが出力されないようにする

```elixir
# 通常ジェネレータで出力されるEnumは空の値も含む
iex(31)> list_of(float()) |> Enum.take(3)
[[], [1.0, 0.0], [-0.625]]
# nonempty/1を入れると空のEnumを除外する
iex(32)> list_of(float()) |> nonempty() |> Enum.take(3)
[[-3.0], [3.0, -1.5], [-4.625, 1.75, -0.75]]
```

## Pros/Cons

Property-Based Testingについてそれぞれメリット/デメリット

### Pros


- 通常プログラムの成長に合わせてテストに期待される入力値も変わってくるが、ランダムな入力値を生成するので入力値の修正が不要
- 期待する入力と出力ではなく、どういう入力値と出力のとき要求が満たされるかを考える必要があるため、プログラムの仕様についての理解が深まる
- より少ない行数で頑強なプログラムを作れる
- テストを回せば回すほど網羅されるのでより確実になる

### Cons

- 思ったよりランダムな値の生成に時間がかかる

試しに最大100個のアイテムを持つlistを100回生成し100回assertしてみたら10秒程かかった
 
```bash
$ time mix test

Finished in 11.5 seconds
1 property, 0 failures

Randomized with seed 833983
mix test  3.88s user 8.52s system 102% cpu 12.108 total
```

`max_runs`オプションで10個のアイテムを10回assertするようにしたら1秒ほどで終わったのでジェネレータによる値の生成でテストにかかるコストを意識した方がいいかもしれない

```elixir
$ time mix test
.

Finished in 0.7 seconds
1 property, 0 failures

Randomized with seed 963450
mix test  1.05s user 0.40s system 111% cpu 1.294 total
```

## Property-Based Testingが向いているもの

どういう状況に向いているか

- エッジケースを攻めたいもの
  - `NULL`、空文字列、マイナスの値、通常入力されなそうな文字列など
- 任意の入力が予想されるもの
  - ユーザ入力のあるAPI
- 取りうる値の範囲が広いもの
- 取りうる組み合わせが多いもの
  - 取りうる値の幅が狭くてもarityが3とか4とか
  - 3個の引数がそれぞれ1〜10の値を取っても引数が一つ増えるだけでオーダーが変わってくる

## Property-Based Testingが向いていないもの

 - 逆に入力と出力が十分想定の範囲に収まるならExample-Based Testingで十分かも

## まとめ

Property-Based Testingについて見ていきました

- Propertyとは → プログラムの特性、性質
- Property-Based Testingではジェネレータを使いランダムな値を生成しテストを実行する
- Example-Based TestingやTDDと対立するものでもないから適材適所で使えばより頑強なプログラムになりそう


## 参考

- [Foundations of Property-Based Testing](http://propertesting.com/book_foundations_of_property_based_testing.html)
- [An introduction to property based testing](https://www.slideshare.net/ScottWlaschin/an-introduction-to-property-based-testing)
- [Abstractivate: Property\-based testing: what is it?](http://blog.jessitron.com/2013/04/property-based-testing-what-is-it.html)
- [stream_data (GitHub)](https://github.com/whatyouhide/stream_data)
- [StreamData: Property-based testing and data generation for Elixir](https://elixir-lang.org/blog/2017/10/31/stream-data-property-based-testing-and-data-generation-for-elixir/)