制約をロジックではなく型で表現する
class: center, middle # 制約をロジックではなく
型で表現する 渋谷Java 第十五回 2016/04/23 --- class: left, middle ## 自己紹介 * 中村 学(Nakamura Manabu) * [@gakuzzzz](https://twitter.com/gakuzzzz) * 株式会社 Tech to Value * Japan Scala Association --- class: center, middle # みなさん
テスト書いてますか? --- class: center, middle ## 僕はテスト書くのが
あまり好きではありません。 --- class: middle # 型 > テスト 偉い人は言いました。 テストで示せるのはバグの存在であって、
バグの不在は証明できない。 型システムはある種のバグの不在を証明できる。 --- class: center, middle そうは言っても、値に関するロジックのテストは必要。 --- class: middle 改めて言いますが、
僕はテスト書くのがあまり好きではありません。 正確に言うと、
テストそのものを書くのはそれなりに好きなのですが、
テストデータをつくるのが大嫌いです。 --- class: middle ## 良くあるケース1 FooStatus: A, B があるとして、
HogeHoge の時は FooStatus: A のものだけ取得する。 ```scala case class Entity(id: Long, foo: FooStatus) ``` --- class: middle ## 良くあるケース1 ``` // fixture Entity(1, FooStatus.A) Entity(2, FooStatus.A) Entity(3, FooStatus.B) Entity(4, FooStatus.B) ``` ```scala it("HogeHogeの時は FooStatus: A のものだけ取得する") { val actual = doHogeHoge() assert(actual === List( Entity(1, FooStatus.A), Entity(2, FooStatus.A) )) } ``` --- class: middle ## 良くあるケース1 ## 機能改修 BarStatus の追加 ```scala case class Entity(id: Long, foo: FooStatus, bar: BarStatus) ``` --- class: middle ## 良くあるケース1 追加したBarStatusのためにレコード追加 ``` // fixture Entity(1, FooStatus.A, BarStatus.X) Entity(2, FooStatus.A, BarStatus.Y) Entity(3, FooStatus.B, BarStatus.X) Entity(4, FooStatus.B, BarStatus.Y) Entity(5, FooStatus.A, BarStatus.Z) Entity(6, FooStatus.B, BarStatus.Z) ``` --- class: middle ## 良くあるケース1 結果 FooStatus: A のレコードが増えて既存テストが fail !! ```scala it("HogeHogeの時は FooStatus: A のものだけ取得する") { val actual = doHogeHoge() assert(actual === List( // 失敗!! Entity(1, FooStatus.A), Entity(2, FooStatus.A) )) } ``` --- class: middle ## 良くあるケース 2 fixtures を利用して、全てのテストでテストデータを共通化してるからさっきみたいな事が起こるのだ。 テスト毎に専用のデータを用意すればテストの独立性が保たれる! 例) S2Unit の Excelファイル等 --- class: middle ## 良くあるケース 2 機能改修が入りました。
テーブルαにカラムが追加になります。 --- class: middle ## 全テストメソッド毎の Excel を
全て修正する必要が! --- class: center, middle こうしてテストが書かれない改修が増えていく…… --- class: middle ## もうテストデータ管理したくない! --- class: middle ## そこで Property Based Test ですよ --- class: middle ## Property Based Test とは テストデータをランダムに半自動生成して、
その全ての値について、
満たすべき性質をきちんと満たしているかテストする。 ```scala 例) property("Listのreverseを2回行うと元のListに一致する") { forAll { (list: List[String]) => assert(list.reverse.reverse === list) } } ``` --- class: middle ## Property Based Test とは Haskell だと [QuickCheck](https://hackage.haskell.org/package/QuickCheck)、
Scala だと [scalaprops](https://github.com/scalaprops/scalaprops) や [ScalaCheck](https://www.scalacheck.org/) という
ライブラリが有名。 Java だと [junit-quickcheck](https://github.com/pholser/junit-quickcheck) や [random-beans](https://github.com/benas/random-beans) や [functionaljava-quickcheck](https://github.com/functionaljava/functionaljava/tree/master/quickcheck) という
ライブラリがあるようですが僕は使ったことありません。 --- class: middle テストデータを半自動生成とは ある型のインスタンスを生成する Generator/Arbitrary を定義。 ```scala case class User(name: String, age: Int) val userGen: Gen[User] = for { name <- Gen.alphaNumStr age <- Gen.coose(0, 150) } yield User(name, age) forAll { (user: User) => ... } ``` --- class: center,middle データの生成方法だけ定義すればよいので、
仕様変更や改修でフィールドが増減しても
その生成方法だけ変更すれば OK --- class: center,middle めでたしめでたし……? --- class: middle ## 制約をロジックではなく
型で表現する --- class: center, middle Property Based Test を書いていくと、
制約をロジックで表しているコードの
Generator/Arbitrary が定義しづらい。 --- class: middle 例えば 設問を3つまで持つことができる簡易アンケートで、
設問種別がA~Eの五種類がある。
ただし設問種別Aだけは、
一つのアンケートで最大1個までしか持つことができない。 ```scala case class Question(qType: QType, subject: String, body: String) case class Enquete(name: String, questions: List[Question]) { require questions.counts(_.qType == QType.A) < 1 } ``` --- class: middle これの Generator を作ろうと思うと大変難しい。 ```scala val questionGen: Gen[Question] = for { qType <- Gen.oneOf(A, B, C, D, E) subject <- Gen.alphaNumStr body <- Gen.alphaNumStr } yeild Question(qType, subject, body) val enqueteGen: Gen[Enquete] = for { name <- Gen.alphaNumStr questions <- Gen.list(questionGen, 3) // A が最大1つまでという条件が書きづらい } yield Enquete(name, questions) ``` 無理やり書くと、テストデータの生成に時間がかかりすぎるようになる。 --- class: middle そこで、制約自体を思い切って型として表現する。 ```scala case class Question(qType: QType, subject: String, body: String) sealed trait Enquete { def name: String, def questions: List[Question] } /** タイプAの設問を1つ持っているアンケート */ case class AEnquete( name: String, aQ: Question, otherQs: List[Question] ) extends Enquete { val questions = aQ +: otherQs } /** タイプAの設問を持っていないアンケート */ case class NotAEnquete( name: String, questions: List[Question] ) extends Enquete ``` --- class: middle こうすることでGenerator/Arbitraryが定義しやすくなる ```scala def questionGen(qType: QType): Gen[Question] = for { subject <- Gen.alphaNumStr body <- Gen.alphaNumStr } yeild Question(qType, subject, body) val notAQuestionGen: Gen[Question] = Gen.oneOf(B, C, D, E).flatMap(questionGen) val aEnqueteGen: Gen[AEnquete] = for { name <- Gen.alphaNumStr aQ <- questionGen(A) otherQs <- Gen.list(notAQuestionGen, 2) } yield AEnquete(name, aQ, otherQs) val notAEnqueteGen: Gen[NotAEnquete] = for { name <- Gen.alphaNumStr otherQs <- Gen.list(notAQuestionGen, 3) } yield NotAEnquete(name, otherQs) val enqueteGen: Gen[Enquete] = Gen.frequency( 1 -> aEnqueteGen, 2 -> notAEnqueteGen ) ``` --- class: center, middle こういったちょっとした制約も、
型で表現することによって、
コンパイル時に間違いを検出できたり、
テスタビリティが高まったりします。 --- class: middle ## まとめ * テストよりまず型 * Property Based Test は機能追加や仕様変更につよい * 制約がロジックで表現されていると、Generator/Arbitraryを作りづらい * 制約を型で表現すると、色々はかどる! ## 補足 * 性質のテスト、は慣れるまで書くのが難しい * 具体的な境界値テストなどは普通のテストの方が楽 * 適材適所を見極め快適なテストライフを --- class: middle ## 質問とか