制約をロジックではなく型で表現する
+ - 0:00:00
Notes for current slide
Notes for next slide

制約をロジックではなく
型で表現する

渋谷Java 第十五回 2016/04/23

1 / 32

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • 株式会社 Tech to Value
  • Japan Scala Association
2 / 32

みなさん
テスト書いてますか?

3 / 32

僕はテスト書くのが
あまり好きではありません。

4 / 32

型 > テスト

偉い人は言いました。

テストで示せるのはバグの存在であって、
バグの不在は証明できない。

型システムはある種のバグの不在を証明できる。

5 / 32

そうは言っても、値に関するロジックのテストは必要。

6 / 32

改めて言いますが、
僕はテスト書くのがあまり好きではありません。

正確に言うと、
テストそのものを書くのはそれなりに好きなのですが、
テストデータをつくるのが大嫌いです。

7 / 32

良くあるケース1

FooStatus: A, B があるとして、
HogeHoge の時は FooStatus: A のものだけ取得する。

case class Entity(id: Long, foo: FooStatus)
8 / 32

良くあるケース1

// fixture
Entity(1, FooStatus.A)
Entity(2, FooStatus.A)
Entity(3, FooStatus.B)
Entity(4, FooStatus.B)
it("HogeHogeの時は FooStatus: A のものだけ取得する") {
val actual = doHogeHoge()
assert(actual === List(
Entity(1, FooStatus.A),
Entity(2, FooStatus.A)
))
}
9 / 32

良くあるケース1

機能改修 BarStatus の追加

case class Entity(id: Long, foo: FooStatus, bar: BarStatus)
10 / 32

良くあるケース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)
11 / 32

良くあるケース1

結果 FooStatus: A のレコードが増えて既存テストが fail !!

it("HogeHogeの時は FooStatus: A のものだけ取得する") {
val actual = doHogeHoge()
assert(actual === List( // 失敗!!
Entity(1, FooStatus.A),
Entity(2, FooStatus.A)
))
}
12 / 32

良くあるケース 2

fixtures を利用して、全てのテストでテストデータを共通化してるからさっきみたいな事が起こるのだ。

テスト毎に専用のデータを用意すればテストの独立性が保たれる!

例) S2Unit の Excelファイル等

13 / 32

良くあるケース 2

機能改修が入りました。
テーブルαにカラムが追加になります。

14 / 32

全テストメソッド毎の Excel を
全て修正する必要が!

15 / 32

こうしてテストが書かれない改修が増えていく……

16 / 32

もうテストデータ管理したくない!

17 / 32

そこで Property Based Test ですよ

18 / 32

Property Based Test とは

テストデータをランダムに半自動生成して、
その全ての値について、
満たすべき性質をきちんと満たしているかテストする。

例)
property("Listのreverseを2回行うと元のListに一致する") {
forAll { (list: List[String]) =>
assert(list.reverse.reverse === list)
}
}
19 / 32

Property Based Test とは

Haskell だと QuickCheck
Scala だと scalapropsScalaCheck という
ライブラリが有名。

Java だと junit-quickcheckrandom-beansfunctionaljava-quickcheck という
ライブラリがあるようですが僕は使ったことありません。

20 / 32

テストデータを半自動生成とは

ある型のインスタンスを生成する Generator/Arbitrary を定義。

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) =>
...
}
21 / 32

データの生成方法だけ定義すればよいので、
仕様変更や改修でフィールドが増減しても
その生成方法だけ変更すれば OK

22 / 32

めでたしめでたし……?

23 / 32

制約をロジックではなく
型で表現する

24 / 32

Property Based Test を書いていくと、
制約をロジックで表しているコードの
Generator/Arbitrary が定義しづらい。

25 / 32

例えば

設問を3つまで持つことができる簡易アンケートで、
設問種別がA~Eの五種類がある。
ただし設問種別Aだけは、
一つのアンケートで最大1個までしか持つことができない。

case class Question(qType: QType, subject: String, body: String)
case class Enquete(name: String, questions: List[Question]) {
require questions.counts(_.qType == QType.A) < 1
}
26 / 32

これの Generator を作ろうと思うと大変難しい。

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)

無理やり書くと、テストデータの生成に時間がかかりすぎるようになる。

27 / 32

そこで、制約自体を思い切って型として表現する。

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
28 / 32

こうすることでGenerator/Arbitraryが定義しやすくなる

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
)
29 / 32

こういったちょっとした制約も、
型で表現することによって、
コンパイル時に間違いを検出できたり、
テスタビリティが高まったりします。

30 / 32

まとめ

  • テストよりまず型
  • Property Based Test は機能追加や仕様変更につよい
  • 制約がロジックで表現されていると、Generator/Arbitraryを作りづらい
  • 制約を型で表現すると、色々はかどる!

補足

  • 性質のテスト、は慣れるまで書くのが難しい
  • 具体的な境界値テストなどは普通のテストの方が楽
  • 適材適所を見極め快適なテストライフを
31 / 32

質問とか

32 / 32

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • 株式会社 Tech to Value
  • Japan Scala Association
2 / 32
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow