# 実践PBT輪読会
## 第2章プロパティを書く
2024/02/06 [@kdnakt](https://twitter.com/kdnakt)
---
## 復習
- 事例ベーステスト
- 入力値と期待値でテスト
- プロパティベーステスト
- ルールをテスト
---
## 今日の範囲
### 第2章プロパティを書く
- 2.1.プロパティの構造
- 2.2.デフォルトジェネレーター
- 2.3.すべてをまとめる
- 2.4.まとめ
- 2.5.演習
----
- プロパティの肝
- 常に真となるルールを見つけること
- ジェネレーター
- ルールを検査する入力値生成機能
- ステートレスとステートフル
- 第1章の例がステートレスプロパティ
- 複雑なステートフルプロパティは第9章で
---
### 2.1.プロパティの構造
- 学ぶこと
- テストすべきルールは何か
- テストに使うデータをどう生成するか
----
#### 2.1.1.ファイル構成
```bash
$ mix new chap2
$ vi ./chap2/mix.exs
# 以下のように編集 ※defpはプライベートな関数
# defp deps do
# [
# {:propcheck, "~> 1.1", only[:test, :dev]}
# ]
# end
```
----
#### テストの初期状態
```bash
$ cat ./chap2/test/chap2_test.exs
defmodule Chap2Test do
use ExUnit.Case
doctest Chap2
test "greets the world" do
assert Chap2.hello() == :world
end
end
```
----
#### プロパティの実例
```bash
$ cat ./chap2/test/chap2_test.exs
defmodule Chap2Test do
use ExUnit.Case
use PropCheck
property "プロパティの説明を書く" do
forall type <- my_type() do
boolean(type)
end
end
# ヘルパー
defp boolean(_) do
true
end
# ジェネレーター
def my_type() do
term()
end
end
```
----
#### 2.1.2.プロパティの構造
```
property "プロパティの説明" do
forall instance_of_type <- type_generator do
property_expression
end
end
```
- type_generator:データ生成関数
- instance_of_type:生成されたデータの変数
- property_expression
- true/falseを返す任意のコード
----
#### 2.1.3.実行モデル
- PropErが行うこと
- ジェネレーターを展開しデータにする
- 展開されたデータをinstance_of_type変数に束縛
- property_expressionがtrueを返すか検証
----
#### シェル(iex)で実験(1)
```
$ cd chap2
$ mix deps.get
( 省略)
$ MIX_ENV="test" iex -S mix
===> Analyzing applications...
===> Compiling proper
Compiling 1 file (.ex)
Generated chap2 app
Erlang/OTP 26 [erts-14.2.1] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.16.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
```
参考 https://jeffkreeftmeijer.com/mix-proper/
----
#### シェル(iex)で実験(2)
```
iex(1)> :proper_gen.pick(:proper_types.number())
{:ok, 3.0633962601395517}
iex(2)> :proper_gen.pick(:proper_types.term())
{:ok,
[
-7.303625927855325,
8,
:"Æü\x0E¡\x03º",
5.884657781859277,
{{}, -9, {}}
]}
```
---
### 2.2.デフォルトジェネレーター
- プロパティの検証効率
- 「どんな入力値が渡されるか」に依存
- ジェネレータはテストの信頼性に影響
- ジェネレーター
- 多くの内部パラメーターを持つ関数
- ランダム性、複雑度合いなどを決定
- 曖昧な型は表現できない
- PID、参照、ソケットなど
----
#### ジェネレーターの例 (1)
generator | データ | 例
-------- | -------- | --------
any(), term() | 任意のErlang項 | 以下のいずれか
atom() | Erlangのアトム | ’ós43Úrcá\200’
※項:データのこと。term。
※アトム:識別子のこと
----
#### ジェネレーターの例 (2)
generator | データ | 例
-------- | -------- | --------
binary() | バイト列 | <<2,203,162>>
boolean() | bool()でも可 | true, false
choose(Min, Max) | Min〜Maxの任意の整数 | choose(1, 10) => 5
float() | 浮動小数点数 | 4.982972307245969
----
#### ジェネレーターの例 (3)
generator | データ | 例
-------- | -------- | --------
integer() | 整数 | 89234
list(Type) | Type型の項のリスト | list(boolean()) => [true, true, false]
non_empty(Gen) | 空でないバイナリorリスト | non_empty(list()) => [abc]
----
#### ジェネレーターの例 (4)
generator | データ | 例
-------- | -------- | --------
number() | 浮動小数点数or整数 | 123
char() | 0〜1114111の文字コードポイント | 65
string() | =list(char()) | "\^DQ\^W\^R/D"
{T1, T2, ...} | タプル | {boolean(), char()} =>{true, 1}
---
### 2.3.すべてをまとめる
- 最初の例「リスト内の最大値を見つける」
- 従来のテスト
```
test "find max" do
assert biggest([1, 2, 3]) == 3
assert biggest([-10, -1, -11]) == -1
assert biggest([0]) == 0
end
# これ以外のエッジケースは?🤔
```
----
#### プロパティを使う
- biggest関数のルールは?
- 渡されたリストの最大の数値を返す
- どのようにコードに落とし込む?
- ソートして最後のものを取る
----
#### プロパティを実装する
```
property "find max" do
forall x <- list(integer()) do
biggest(x) == List.last(Enum.sort(x))
end
end
# 明らかに間違った実装
# head: リストの先頭の要素
# tail: リストの先頭以外の要素
def biggest([head | _tail]) do
head
end
```
----
#### テスト結果 :cold_sweat:
- 空リストで失敗
```
$ mix test
(コンパイル時のログとか。省略)
1) property find max (Chap2Test)
test/chap2_test.exs:11
Property Elixir.Chap2Test.property find max() failed. Counter-Example is:
[[]]
(スタックトレースとか。省略)
Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 doctest, 1 property, 1 failure
Randomized with seed 321095
```
----
#### テストを修正する
```
property "find max" do
forall x <- non_empty(list(integer())) do
biggest(x) == List.last(Enum.sort(x))
end
end
```
- 再テスト前にキャッシュの削除が必要
- `_build/propcheck.ctex`
----
#### 再テスト結果 :sweat:
- 2つ目の要素が1つ目より大きい場合に失敗
```
$ mix test
(省略)
1) property find max (Chap2Test)
test/chap2_test.exs:11
Property Elixir.Chap2Test.property find max() failed. Counter-Example is:
[[-1, 0]]
(省略)
Finished in 0.05 seconds (0.00s async, 0.05s sync)
1 doctest, 1 property, 1 failure
Randomized with seed 167976
```
----
#### 関数を修正する
```
def biggest([head | tail]) do
biggest(tail, head)
end
defp biggest([], max) do
max
end
defp biggest([head | tail], max) when head >= max do
biggest(tail, head)
end
defp biggest([head | tail], max) when head < max do
biggest(tail, max)
end
```
----
#### テスト結果 :tada:
```
$ mix test
..
Finished in 0.2 seconds (0.00s async, 0.2s sync)
1 doctest, 1 property, 0 failures
```
---
### 2.4.まとめ
- 学んだこと
- ステートレスプロパティの基本的構文
- データを記述するジェネレーター
- テストの実行方法・デバッグ方法
---
### 2.5.演習
- 問題1
- ジェネレーターが生成したデータのサンプルを取得する関数は?
----
- 問題2 何をテストしようとしている?
```
property "問題2" do
forall (start, count) <- {integer(), non_neg_integer()} do
list = Enum.to_list(start..(start + count))
count + 1 == length(list) and increments(list)
end
end
def increments([head | tail]), do: increments(head, tail)
defp increments(_, []), do: true
defp increments(n, [head | tail]) when head == n + 1,
do: increments(head, tail)
defp increments(_, _), do: false
```
{"title":"実践PBT輪読会 第2章","description":"2024/02/05 @kdnakt","slideOptions":"{\"transition\":\"slide\"}","contributors":"[{\"id\":\"df36d0f0-b67e-41ac-96b3-f3988326d230\",\"add\":6698,\"del\":893}]"}