# 実践PBT輪読会 ## 第5章 信頼できるテスト (1) 2024/09/10 [@kdnakt](https://twitter.com/kdnakt) --- ## 第II部 ### 実践で学ぶステートレスプロパティ - 第5章 信頼できるテスト - 第6章 プロパティ駆動開発 - 第7章 収縮 - 第8章 標的型プロパティ --- ## 今日の範囲 ### 第5章 信頼できるテスト - 5.1 仕様 - 5.2 プログラムの構造に関する考察 - 5.3 CSVのパース ※5.4以降は次回 ---- - 最初は大した意味のないプロパティ書きがち - 適する事例、適さない事例を知ろう - Birthday Greeting Kata演習問題 - データ保存、文字列解析、文字列整形etc - 本章ではユニットテストに焦点 - 新規Prjにプロパティを追加する感覚をつかむ --- ### 5.1 仕様 - 誕生日の従業員にお祝いメールを送信する - 従業員名簿のサンプル ```csvpreview {header="true"} last_name, first_name, date_of_birth, email Doe, John, 1982/10/08, john.doe@foobar.com Ann, Mary, 1975/09/11, mary.ann@foobar.com ``` - 誕生日メールの例 ``` Subject: Happy birthday! Happy birthday, dear John! ``` ---- #### 追加の制約 - ユニットテストのみ - DB通信、ファイルシステムはNG - 仕様変更予定 - CSV形式を使い続けるわけではない - メール送信含め、実装の変更が最小の修正で済むように --- ### 5.2 プログラムの構造に関する考察 - ErlangやElixirは関数型 - 副作用のあるコードはまとめる - それ以外は純粋に保つ ---- #### このプログラムに必要な操作 | 副作用あり操作 | 関数プログラミング的な操作 | | ----------- | -------- | | ファイルの読込 | CSVを従業員名簿に変換 | | 今日の日付取得 | 日付から従業員検索 | | メールを送信 | メールを文字列として整形 | ---- #### 全体設計 ![スクリーンショット 2024-09-10 0.56.07](https://hackmd.io/_uploads/S10sc92nA.png) ---- #### 各コンポーネント - CSVパース:Erlangの項をマップに変換 - フィルター:日付や時間ベース - 従業員モジュール:CSVパースとフィルターをまとめる - メールテンプレート ---- #### mixプロジェクトのセットアップ ```bash! $ mix new bday * creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs (省略) ``` - mix.exsファイルを編集しpropcheckを追加する - 参考:https://github.com/kdnakt/pbt-elixir --- ### 5.3 CSVのパース - CSVの正しいプロパティとは? - RFC4180は単純な仕様(informative) - CRLFで区切られたレコード - 空白はレコードの一部 - 改行,二重引用符,カンマを含む値は二重引用符で括らなければならない etc... ---- #### CSVの実態はめちゃくちゃ https://x.com/Nkzn/status/1829118019706183991 ![スクリーンショット 2024-09-09 23.27.29](https://hackmd.io/_uploads/S1Vy8F22A.png) ---- #### CSVの仕様を実装する - 第3章で学んだ手法を試す - モデル化 - 事例テストの汎用化 - 不変条件 - 対称プロパティ - 特に対称プロパティ:シリアライズとデシリアライズを同時にテスト - ---- #### まずは単純な文字列のレコードのテストを書く - elements()のリストは先頭に向かって収縮する - 反例の可読性を上げられる ![スクリーンショット 2024-09-10 0.57.41](https://hackmd.io/_uploads/r1tbjq22A.png) ---- #### レコードを組み合わせる ![スクリーンショット 2024-09-09 23.45.36](https://hackmd.io/_uploads/HkGXqtnhC.png) ---- #### csv_source()ジェネレータ ![スクリーンショット 2024-09-10 0.03.06](https://hackmd.io/_uploads/SyDSCtnhA.png) ---- #### シェルで動作確認 ```bash $ MIX_ENV="test" iex -S mix Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] Interactive Elixir (1.17.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> c("test/csv_test.exs") (省略) [CsvTest] ``` ```bash iex(2)> :proper_gen.pick(CsvTest.csv_source()) {:ok, [ %{"wi" => "3DwuRpof0", "y)(@[GW47M" => "K;L[m-'"}, %{"wi" => "V+xc\\", "y)(@[GW47M" => "[o"}, %{"wi" => "Oe", "y)(@[GW47M" => "Zx.z(t"}, %{"wi" => "r#nF=-av7", "y)(@[GW47M" => "fqn5"}, %{"wi" => "u\\p", "y)(@[GW47M" => "l3"} ]} ``` ---- #### CSVパーサーの対称プロパティ ```elixir property "Encode and Decode" do forall maps <- csv_source() do maps == Csv.decode(Csv.encode(maps)) end end ``` ※メインはテストの書き方。パーサーの実装は省略 ---- #### 興味深いパース失敗例 - `\n\ra` - 長さ0の文字列を値として持つレコード - デコード期待結果:`[%{"" => "a"}]` - 1列だと次の行の有無が判断できない - csv_source()を変更して列を2以上に ```elixir! def csv_source() do let size <- pos_integer() do let keys <- header(size + 1) do list(entry(size + 1, keys)) end end end ``` ---- #### 既知のバグはユニットテストとして記録 ```elixir! test "1 列の CSV ファイルは本質的に曖昧 " do   assert "\r\n\r\n" == Csv.encode([%{"" => ""}, %{"" => ""}])   assert [%{"" => ""}] == Csv.decode("\r\n\r\n") end ``` - 現在の実装では実現できないケース - 事例ベースの手動テストも価値がある - セーフティネット+リグレッション ```elixir # RFC から引用した反例。 # マップには重複キーがないので、現在の実装ではうまくいかない test "dupe keys unsupported" do csv = "field_name,field_name,field_name\r\n" <> "aaa,bbb,ccc\r\n" <> "zzz,yyy,xxx\r\n" [map1, map2] = Csv.decode(csv) assert ["field_name"] == Map.keys(map1) assert ["field_name"] == Map.keys(map2) end ``` --- ### まとめ - 副作用のない関数プログラミングの操作をに分割する - パーサーの場合、対称プロパティが有効 - プロパティだけでなくユニットテストも活用しよう --- ### To Be Continued... - 5.4 レコードのフィルタリング - 5.5 従業員モジュール - 5.6 テンプレート - 5.7 すべてをつなぎ合わせよう - 5.8 まとめ
{"title":"実践PBT輪読会 第5章 信頼できるテスト (1)","slideOptions":"{\"transition\":\"slide\"}","description":"2024/09/10 @kdnakt","contributors":"[{\"id\":\"df36d0f0-b67e-41ac-96b3-f3988326d230\",\"add\":4873,\"del\":518}]"}
    217 views