読者です 読者をやめる 読者になる 読者になる

dogというScala用テストフレームワーク(?)を作ってみた

https://github.com/pocketberserker/dog

このフレームワークPersimmon の移植というか Persimmon に存在していた型をよりMonadとかそういう方面に倒してみようというものです。

テストという存在の型について考えてみる - pocketberserkerの爆走

上記に途中経過は書きましたがそれから数日と経たずにいくつか変更が入ってます。

特徴

  • type TestCase[A] = Kleisli[TestResult, Endo[Param], A]
  • type AssertionResult[A] = NotPassedCause \/ A
  • scalapropsフレンドリー
  • イミュータブル、ミュータブルを意識しましょうてきな

モジュールとかその他リポジトリについて

  • dog-core
    • 基本となる型と関数をまとめたもの
    • TestCase単体でタイムアウト設定して実行できるように scalaz.concurrent に依存している
    • 上記の通り TestCase は Kleisli なので合成できる
    • AssertionResult を合成することでいわゆる Soft Assertion を実現している
  • dog
    • sbtのtest-interfaceに依存
    • scalapropsの実装をfork(実際にはコピーしたけど)して改修
    • scalapropsとは異なり非同期実行しても問題ないつくりになっているがさてはてどうしたものやら
  • gen
    • scalaprops-genに依存
    • scalaprops.Gen を使って入力値だけいい感じに生成してもらう
    • 入力と期待値の集合は作れるけど性質までかけそうにない場合とかに…ぶっちゃけパラメタライズテストのかわり
    • 固定値で回すくらいなら数撃ってあてようの方針
  • props
    • scalaprops-coreに依存
    • PropertyやPropertiesとTestCaseの変換を目的としている
  • sbt-dog 9割方sbt-scalapropsのコピー。scalaprops様様である(MITライセンスで助かった…)
    • sbt-scalapropsに比べてぴーきーな挙動になりやすい
  • dog-examplesサンプル集。雰囲気がつかめるかも?

ここまでで実装期間6日です。 正直ScalazとScalapropsがなければここまで楽に実装はできなかったと思うので xuwei-kさん ++

作った理由を改めて

  • テストの合成とか型とかについてちゃんと検証したかった(某交流会の資料作成絡み)
  • PersimmonのVS拡張や某ライブラリの移植に難航しているので息抜きしたかった
  • 深夜のテンション

名前はリポジトリ作るときに"ドッグフーディングしたい"というのと"Co猫"という謎ワードがふってきて悩んだ結果であり、特に猫に張り合っているわけではないです。

今後の計画

  • Property#forAll などをラップして性質を満たす時にGen#sampleをテスト結果として束縛できるようにしたい
  • Scala.js対応
    • とはいえこれはリフレクションを使って実装している現状うまくいかない
    • なので、scalaprops共々forkして別リポジトリで実装するつもり
  • やりたいことあればてきとーに

こういうものがほしい、とかあればissueに登録していtだけるとうれしいです。

ScalaMatsuriにこれで応募してみるという手もあるが、さてはて。

テストという存在の型について考えてみる

いわゆるユニットテストフレームワークに現れる各種型について考えてみる。

成果は考えない。

このエントリは考えがまとまっていない段階で書かれたものであるため、間違っている可能性があります。

成果物(の途中経過)

途中結果を素っ飛ばした作成中のものは下記に公開している。

https://github.com/pocketberserker/dog/tree/d0ad112e00c72500a2ac5df764b7109249e684de

ドッグフーディングしたいのでこの名前になっただけである*1

この記事では Scala with Scalaz と 一部 FSharp(というか Persimmon )を使って考察する。

テストケースは Kleisli

テストケースは入力値、実行事前条件、期待値、実行事後条件から構成されるはずだ。 このうち入力値、実行事前条件、実行事後条件は型の内部に隠せるものなので、たぶんよくあるテストケースはこんな感じになる。

trait TestCase {
  def run(): TestResult = {
    ???
  }
}

しかし、テスト結果を合成したい場合にはこれは圧倒的に情報が足りないので、テスト結果に値を持てるようにしたほうがよさそうである。

trait TestCase[A] {
  def run(): TestResult[A] = {
    ???
  }
}
// 型コンストラクタはおいおい
trait TestResult[A]

さて、これでTestCase#run は 引数なしで実行できTestResult[A]を返す操作が行える存在になった。

ここでちょっと考えると、引数なしというのは Unit を引数にとる、と考えられなくもない。 そうすると都合の良いことに、Kleisliの構造に一致する。

import scalaz._

type TestCase[A] = Kleisli[TestResult, Unit, A]

// ちょっと不恰好だが、以下のように実行できる
// testcase.run(())

テスト結果はMonadにできなくもない

テスト結果は多くの場合、成功か失敗かで表されるはずだ。

case class Success[A](value: A) extends TestResult[A]
case class Failure[A](reason: String) extends TestResult[A]

しかし、これは色々と大雑把な構造になっている。

  1. エラーが考慮されていない
  2. 失敗時の情報が蓄積できない
  3. "失敗"なのか"テストをスキップ(無視)した"のか判断がつかない

問題1を解決するためにエラーを持てるようにする。

case class Done[A](result: String \/ A) extends TestResult[A]
case class Error[A](e: Throwable) extends TestResult[A]

テスト実行完了か、エラーが発生したことが確認できるようになった。 この時点では明らかにMonadである。

問題2について解決を試みる。

case class Done[A](results: ValidationNel[String, A]) extends TestResult[A]
case class Error[A](e: Throwable) extends TestResult[A]

成功時に保持すべき値は最終結果ひとつで良いのに対し、失敗情報は多数保持すべきなのでValidationNelがちょうどよさそうだ。 ただし、実装次第ではMonad則を満たさなくなりそうである。

問題3が残っている。 これはValidationNelだけでは解決できそうにないので、別の型を導入する。

trait Cause
case class Violated(reason: String) extends Cause
case class Skipped(reason: String) extends Cause
trait AssertionResult[A]
case class Passed[A](value: A) extends AssertionResult[A]
case class NotPassed[A](cause: Cause) extends AssertionResult[A]
type AssertionNel[A] = NonEmptyList[AssertionResult[A]]
case class Done[A](results: AssertionNel[A]) extends TestResult[A]
case class Error[A](e: Throwable) extends TestResult[A]

Done#results に入る値を特定のものに絞り(ありえない状態を除外する、といったほうが正しい)、それ以外の入力は受け付けないようにすればTestResult[A]Monad則を満たす。 しかしその実装はMonadと呼べるのかというのは、疑問が尽きない。

Persimmon の TestResult は Monad ではない

今回はエラーだった場合はそれ以降何もしないことにしたので、Monadになる可能性があった。 しかし、実行できるアサーションはすべて実行してしまえ、という方針にするとMonad則を満たせなくなる。

Persimmon はそういう方針なので、おそらくMonad則を満たさない。

AssertionNel ~> TestResultは実装できるか

これに関しては考え方次第である。

  • テストケース名や入力値をテスト結果に持たせたいなら、無理
  • 変換時は空のデータを持たせ、Runnerで後から差し替えるならできそう

検証が足りていないのでもう少し考慮すべき項目があるかもしれない。

Persimmonに関していえば、変換できなくもない。 テスト名が空な場合にはリフレクションで変数(関数)名前を取得して入れ替える機能が備わっているためだ。

Assertion[A]は必要か

以下のような型を定義できる。

type Assertion[A] = Kleisli[AssertionResult, Unit, A]

しかし必要性を感じない。 基本的にさっさと実行されてしまうため、機能過多に思える。 これが何かに使えるか考えるなら、AssertionResult[A]の機能拡充を考えたほうがよさそうである。

TestResult[A] は Applicative であるべきか

現時点のTestResult[A] は入力値を制御しているためMonad則を満たすが、Applicativeにとどめておくべきかどうかは悩ましい問題だ。

Scalaのfor式は F# のコンピュテーション式に比べて表現に不安を感じるし、テスト失敗情報を蓄積させるならMonadにすべきではないのはその通りだ。 しかし、ScalaだとApplicativeで合成するのはそれはそれで面倒くさい作業にしかならない気もしている。

書きやすいDSLを実装する、という点ではScalaではマクロを使うことも考慮すべきなのだろうが…さてはて。

まとまらない

  • FreeやFreeApを使って何かできないか考えていたらいつのまにかこういうものに行き着いた、なぜ
  • Operational monad などの利用を考えると、やはりHaskellのライブラリを移植したほうが早いのだろうか…
  • とはいえ、Scalazの各種型で色々定義できそうということがわかっただけでも面白かったので良しとする

*1:猫の双対でco猫とかにしてみたかったけどどう見ても名前負け思想だったのでやめた

#TestingFrameworkMeeting というイベントをやります

github.com

概要や参加方法諸々は上記リポジトリに書いてあります。

ユニットテスティングフレームワークや関連のあるものについて議論してみてもいいんじゃないかな、と思って企画しました。 (なお、ここまでこぎつけたのは企画を一緒に練ってくださったTDD Base Campの皆様のおかげです。いつもありがとうございます)

若干ハードル高めですが、濃いイベントなのは間違いないのでお待ちしております。

"なぜshapelessにはGeneric[Boolean]がないの?"と尋ねてみた

Gitterで質問したのでそのメモ*1。 なお私の英語は壊滅的なので片言?なのは許してください。

Hello. Why does Generic[Boolean] instance not exist?

と投稿したところ、milessabinさん(shapeless作者)から

Because there's no obvious sum of product representation of Boolean.
I guess you would want false.type :+: true.type :+: CNil?

と返答があった。なるほど。

@milessabin Thank you. I understand.
I try to port from Haskell code(using GHC.Generic) to Scala code. GHC.Generics derive some instances of Generic a.

とりあえずこういうことしようとしたんだよーと言ったところ

@pocketberserker gotcha.
The idiom here is to hand-write instances for primitives and use Generic for ADTs.
I would be very interested to see a side-by-side comparison of something written with GHC generics and something written with shapeless.
Would you be able to blog about it?

ほえーと思ってたらなんか後日記事かくことになった。

まぁ、最初から無理っていうのもなんだし、やるだけやってみよう…。

*1:という名のバックアップ

「Persimmonでテストを書く」での疑問点に回答してみる

F#

Persimmon でテストを書く — a wandering wolf

上記記事で2点ほどあがっていたものがあるので。

Usingがない

検証さぼってただけです、すみません。pull requestだしたのでそのうち入るかもしれません。

enable use keyword by pocketberserker · Pull Request #85 · persimmon-projects/Persimmon · GitHub

ただ、useで束縛した値をreturnすると思わぬ落とし穴にはまると思うので、Disposableな値は返さないように気を付けたほうが良いと思います。

Zeroがない

Zero、わりと影響範囲が大きいので悩みどころですね。

単純にunitな式を実行したいなら

// hoge: unit -> unit
return hoge ()

もしくは

do hoge ()
return ()

と書けばよいと思います。

面倒くさいなら

type TestBuilder with
  member __.Zero() = ...

と型拡張を書くとかですかね。

あの時を振り返る

ふと、 初回TDDBC名古屋 が開催されてから5年が経過したことに気がついた。

あの時はブログをやっていなかったので感想記事を書けなかったが、良い機会だし、感傷に浸りたい年頃になってしまったので、駄文(になるだろう)を書いてみる。

なお、これを読んでも得られるものはないだろう。

参加したきっかけ

あれは大学4年になるかならないかという時期、指導教員の [twitter:mikantsuki] と相談してTDDに関連する研究にしようと決まった時のこと。

「そういえば、TDDに関する勉強会が北陸であったらしいんだけど、それが次は名古屋で開催されるみたいだね。 遠いから無理かもしれないけれど、行けるなら参加したら?」

勉強会という存在を始めて知った瞬間である。 きっと良い経験になるだろうと思ったので、行く決意を固める。

今考えると、よく思い切ったものだ。

登録した時期

応募がはじまったよ、と教えてもらったので早速登録しようとする。

twitterアカウントを持っているなら記載してもらえると嬉しい、とある。 持っていなかったので作った。

その頃に研究でデータがほしいから「データ取りに協力していただけると〜」みたいなことをつぶやいた記憶がある。 若干黒歴史だが…。

ちなみに id:t-wada さんのtwitterアカウントを始めてみた時の感想は「サッカー好きなのだなー」。 その頃はちょうどW杯が始まるくらい時期だったので、たぶんそれ系のつぶやきが目に入ったのだろう。

id:bleis-tift さんは、もっと年上な人を想像していた。

初日

ここからはちょっと長くなるかもしれない(後書き:そんなに長くならなかった)。

会場到着あたりまで

初名古屋。 若干迷いながら会場に到着する。

会場の1Fにそれらしき人々を見つけるも、怖くてしゃべりかけることができない。 黙々と開場まで待つ。 (どうでもいいが、この時の愛機はIS01だった)

初勉強会なので勝手がわからず、てきとーに空いてる席に座る。

午前

飛び交っている単語がわからないものばかりだ。

OCaml?なんだそれは?*1

不安が募る。 ただ、タイミングよくイベントが開始されたので講演に集中する。

「なるほど、そういうことか」とか「ここはみかままが講義で言ってた気がする」 とか思ったりなどしていた。

ペアプロデモはRubyでわからないところがでてきたりしたのでその場で調べていたりした気がする。

ペアプロ

お昼ご飯を食べ、ペアプロ前半。 お題が講義の課題とにていたためその時の記憶に引っ張られてしまったなーと反省した記憶がある。

ペアプロ後半。 Mock の存在を知ったのはこの時だ。

お題が進み、時刻の概念が現れた時にペアの方が「これはMockを使うべきなのかな」と言ったのだ。

初めて聞く単語だったので、何をしていいか全然わからない。 わからないから、「わからないので、使い方を見せてください」とペアプロを放棄してお願いした。 ペアプロ体験が目的のイベントでこれを言っていいのかと迷ったが、興味の方が勝った。

レビューあたり

このあたりで、自分の力のなさに絶望していた。 あのくらいプログラミングできないと就職してから仕事できないのか…みたいな。

懇親会

知らない人ばかりだったため、なかなか話しかけることができずわりとぼっちになってたところ、bleisさんやt-wadaさん、kokubo_yusukeさんが話しかけてくださった。 これがなければ気分が沈みっぱなしだったと思う。 喋っているうちに心が軽くなる。

懇親会後は会場に戻ってnagiseさんのGenerics談義をきいていた。 この話もとても面白かった。

デモ用にレガシーコードを提供したのはこの辺りだ。 bleisさんとt-wadaさんがデモ用のちょうど良いサイズのコードがなくて困っているという話をしていた。 ここで自分のPCに使えそうなコードがあることに気づき、懇親会で話掛けてくださったことへのせめてもの感謝の気持ちとして、恥を捨ててコード提供を提案した。

なお件のレガシーコードは、現在はデモ用として一部構成をいじった上でGitHub上で公開されている。コードに関してはそのままなので、レガシーコード改善の練習として触ってみるのも一興かもしれない。

tddbc/LegacyCodeJava · GitHub

しかしひどいな、これ。 今ならparser combinatorで一発事案だ…制約がある中で書いたコードとはいえ、これはひどい

余談だが、もともとこのコードにはテストコードが存在していた。 探せばでてくるかもしれないが、JUnit3で書いていたし古傷をえぐるだけだろうと思い掘り返さないでいる。

宿泊部屋

修学旅行みたいに何人かで集まってワイワイコード書いてた。 この時に何名かの方が研究データ取りに協力してくださった。

2日目

寝たら気分すっきり、気力が回復する。

朝食の席ではコンパイラについておしゃべりしていた記憶がある。 そして赤味噌うみゃー。

講演とデモ

講演とデモ。 デモでコードがどんどん改善されていくのはとても勉強になった。

昼食はうどん・そば・ごはんという謎コンボ。

グリーンバンドの話が持ち上がっていたのはこの時だっただろうか。 購入者を募っていたが、私は距離が距離だったためさすがになと思って購入を控えた。 …正直言うとちょっと後悔した。

午後

最初は苦戦しつつ、途中から慣れてきてテストを差し込んだりあれこれ相談したりしていた気がする。

ところで、作業中にきがついたことの一つに、OCamlScalaでコードを書いていた方々が感動するくらい良い笑顔だったというのがある((一番記憶に残っているあの顔はきっとmzpさんだ))。 関数型言語=大学で習ったけどよくわからなかったし怖い*2、と思っていた私にはわりと衝撃的だった。 この衝撃が、今の私を形成するに至った要因の一つである。

レビューの際もわからない単語だらけだったが、この時は開き直っていた。 「自分のレベルの低さに絶望したことなら前にもあるじゃん。メモして今から調べてやればいいんだよ」みたいな。 厨二病全開だったりもした。「絶望は人を次のステージへと昇華させる…」

帰りは時間の都合があったため、ささっと退散。

帰り

t-wadaさんの言葉が脳内でリフレイン。

『TDDBC は「やりたい」と手をあげれば始められるイベントです。』

この濃度のものは、今はおそらくこのイベントを開催しなければ体験できない。 彼らを巻き込みたい。 私より数段できる彼らなら、きっとそれを糧にするだろうから。

気がついたらtwitterで「佐賀でもやりたいなー」と書き込んでいた。

そして新幹線内で爆睡し、特急電車に乗り換えてさらに爆睡し、帰宅後は熟睡した。

振り返って

思い出しながら書いていたら「若かったなー」と思ってしまうあたり、おっさん度が上がっている。

得たことはたくさんあるが、

  • 知らなかったことを後悔するよりさっさと調べる
  • やりたいと思ったらやれ
  • 動いていれば何かにつながる

あたりは特に今も自分の生活や勉強会開催のモチベーションにつながっている。

ありがとう、かつての私。 あの時参加していなければ、絶望していなければ、ここには居ないし今も技術に対して足掻いていることはなかっだろう。

*1:当時はまだ存在を知らなかった

*2:今となっては食わず嫌いなだけだったと反省するばかりだ

JavaScriptのパーサコンビネータparsimmonをTypeScriptで使ってみる

時代の流れに逆らえなくなってきた感があるので、TypeScriptの練習。

とはいえ、最初は慣れ親しんだものから攻めるのが現実的だと思われるので、パーサコンビネータを題材にする。

jneen/parsimmon · GitHub

ライブラリはこれを使う。 自作するかとも考えたが、特に車輪の再発明する理由がなかったので採用。

お題

FParsecで遊ぶ - 2つのアンコール

このオフサイドルールな入力を解析する。

出来上がり

https://github.com/pocketberserker/sample-parser/tree/3c3f2ea57e0dc647dc88dedb32e6cf1c113af5cb

思ったこと

  • tuple を使うと型が推論されてもよさそうな部分で推論されなくなることがあった気がするので、あまり使用しないほうがいいのか?
  • 現時点のDefinitelyTyped に登録されている d.ts が最新に対応していないのでちょっと自分で定義してあげる必要がある
  • ひたすらメソッドチェーンなので気を付けないとインデントが…
  • array.sliceしまくっているけど、これいいのだろうか…?

余談

実は最初のほうはJavaScriptでも書いていたのだが、テスト実行時に「hogefuga not found」を連発されて降参した。 やっぱりコンパイルがないと無理だったよ…。