# 実践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}]"}
    249 views