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