Magnolia Tech

いつもコードのことばかり考えている人のために。

Javaでファイルの内容からMIME Typeを推測する方法

前回のエントリの続きです。

blog.magnolia.tech

前回はファイル名から…つまり拡張子をもとにMIME Typeを特定する方法を紹介しましたが、今回はファイルの内容からMIME Typeを推測する方法です。

Files.probeContentType

前回も紹介した下記のブログエントリに、カスタマイズ方法が記載されていますが、Files.probeContentTypeMIME Typeを決定するアルゴリズムはカスタマイズ可能で、java.nio.file.spi.FileTypeDetectorを継承したクラスを作成し、サービスとして登録します。

waman.hatenablog.com

デフォルトのアルゴリズムでは、ファイル名を元にMIME Typeを決定しているので、独自にロジックを書けばファイルの内容からMIME Typeを推測することが可能になります…が、ちょっと使うにはかなりハードルが高いですし、汎用的に作ろうとすると異常な努力が必要になってくるので、アプリケーションの仕様から特定のフォーマットを識別したい、という時以外は採用は厳しそうです。

URLConnection.guessContentTypeFromStream

URLConnection.guessContentTypeFromNameはファイル名(拡張子)からMIME Typeを特定していましたが、java.io.InputStreamからの入力(つまりデータの中身そのもの)からMIME Typeを推測するメソッドがURLConnection.guessContentTypeFromStreamです。

いきなりopenJDKのソースへのリンクを張っておきますが、実装を見ると主にtext/htmlapplication/xmlimage/gifimage/pngimage/jpegあたりの判定には使えそうです(それ以外は…なぜそれを判定対象にした?という感じの並びですね…)。

github.com

判定ロジックがべた書きされているので、カスタマイズ方法は無さそうです。サブクラスを作れば別ですが、そうすると完全にロジックを書かないといけないので、これまた異常な努力が必要になりますね。

おわりに

ファイルの内容から推測する方法は、あまり汎用的な方法は(当然ですが)無いので、URLConnection.guessContentTypeFromStreamを使う以上の汎用性は求めない方が良さそうです。

スッキリわかるJava入門 第2版 (スッキリシリーズ)

スッキリわかるJava入門 第2版 (スッキリシリーズ)

ファイル名からMIME Typeを教えてくれるJavaのライブラリ

ScalatraのMIME Type関係の機能が盛大に壊れている訳ですが、じゃあそれを直すためにMIME Typeを教えてくれるライブラリについて調べてみました。標準ライブラリだけでもいくつか方法が有りますね。

Files.probeContentType

Java7から導入されたFiles.probeContentTypeを使うと、簡単にMIME Typeを取得できます。

scala> import java.nio.file.Files
import java.nio.file.Files

scala> import java.nio.file.Paths
import java.nio.file.Paths

scala> val htmlfile = Paths.get("test.html")
htmlfile: java.nio.file.Path = test.html

scala> Files.probeContentType(htmlfile)
res2: String = text/html

scala> val cssfile = Paths.get("test.css")
cssfile: java.nio.file.Path = test.css

scala> Files.probeContentType(cssfile)
res3: String = text/css

scala> val jsfile = Paths.get("test.js")
jsfile: java.nio.file.Path = test.js

scala> Files.probeContentType(jsfile)
res4: String = text/javascript

最後、javascripttext/javascriptになっていますが、application/javascriptで返して欲しいですね。カスタマイズは可能です。下記のブログエントリが参考になります。

waman.hatenablog.com

返って来る値が実装依存なので、自分が使いたいMIME Typeはきちっとテストを書いておきましょう。

URLConnection.guessContentTypeFromName

URLConnection.guessContentTypeFromNameもファイル名からMIME Typeを取得できます。

scala> import java.net.URLConnection
import java.net.URLConnection

scala> URLConnection.guessContentTypeFromName("test.html")
res5: String = text/html

scala> URLConnection.guessContentTypeFromName("test.css")
res6: String = null

scala> URLConnection.guessContentTypeFromName("test.js")
res7: String = null

デフォルトでは、かなり残念な結果になりました。こちらもMIME Typeはカスタマイズ可能です。こちらの記事によると、content-types.propertiesというファイルを用意すれば良さそうです。

Getting and Extending mime-types in Javathilosdevblog.wordpress.com

content-types.propertiesの正式なフォーマットの解説が見つけられませんでしたが、こちらのブログにサンプルファイルが紹介されていました。

alvinalexander.com

javax.activation.MimetypesFileTypeMap

最後はjavax.activation.MimetypesFileTypeMapです。

scala> import javax.activation.MimetypesFileTypeMap
import javax.activation.MimetypesFileTypeMap

scala> val mimeTypesMap = new MimetypesFileTypeMap
mimeTypesMap: javax.activation.MimetypesFileTypeMap = javax.activation.MimetypesFileTypeMap@7e258a05

scala> mimeTypesMap.getContentType("test.html")
res8: String = text/html

scala> mimeTypesMap.getContentType("test.css")
res9: String = application/octet-stream

scala> mimeTypesMap.getContentType("test.js")
res10: String = application/octet-stream

こちらもcssjavascriptが正しく扱えていません。カスタマイズ方法は公式ドキュメントにきちんと書かれています。

MimetypesFileTypeMap (Java Platform SE 8)

しかし、残念ながらこのパッケージは将来廃止が予定されています。

JEP 320: Remove the Java EE and CORBA Modules

これから新しく使うには相応しくないですね。

おわりに

ファイル名からMIME Typeを取得する方法を紹介しました。標準ライブラリだけでも3種類有って、更にそれぞれカスタマイズ方法が違うってなかなかカオスな感じですね。

ScalatraのMimeTypesは、ファイルの内容に基づいてMIME Typeを推測する機能も有るので、次回はその方法について紹介します。

スッキリわかるJava入門 第2版 (スッキリシリーズ)

スッキリわかる Java入門 実践編 第2版 (スッキリシリーズ)

スッキリわかる サーブレット&JSP入門 (スッキリシリーズ)

Scalatra-JSONの使い方

ScalatraのJSONサポート

ScalatraにはJSONのサポートが用意されていていて、JSONのリクエストやレスポンスにまつわる種々の機能を提供してくれます。

一応公式ドキュメントにはざっと使い方が説明されていますが、ちょっと端折り過ぎ感が有るのと、内部構造を理解した方がメリットをより理解できる部分も有るので、改めて使い方をまとめてみました。

長くなってきたので、今回は環境準備と、リクエストのJSONをパースするところまでです。JSONのレスポンスを生成する方は次回に。

2018/5/20 18:30追記

Json4sがパースエラーを起こした時の挙動が正確ではなかったので、修正しました。例外はWebアプリケーションまで伝搬せず、JNothingが返って来るだけです。

環境準備

プロジェクト作成

まずはsbt newコマンドでScalatraプロジェクトのひな形を用意します。

$ sbt new scalatra/scalatra.g8

色々と質問が出てきますが、今回は全てデフォルトのままで進めましょう(つまり、全てリターンキーを押下して進める)。

ちなみにsbt newコマンドはgitリポジトリの作成までは面倒を見てくれないので、プロジェクトが作成されたら忘れずにgit initをしておきましょう。

build.sbtの追記

build.sbtに必要なアーティファクトを追加します。

ScalatraのJSONモジュールはscalatra-jsonという別モジュールで提供されているので、まずはそれを追加します。バージョンはScalatra本体に必ず合わせて下さい。

またscalatra-jsonjson4sをベースで作られていて、JSONパーサとしてJacksonか、Lift-Json由来のNativeの2種類のうち、どちらかを選択して使用するようになっています。

どちらを使用するかに合わせてbuild.sbtに下記のアーティファクトを追加します。

  • Jacksonをパーサに使う場合は、json4s-jacksonを指定
  • Nativeパーサを使う場合は、json4s-nativeを指定

なお、json4sのバージョンはScalatraが内部で指定しているバージョンに合わせる必要が有ります。例えばScalatra 2.6.3はjson4s 3.5.2を採用しているので、同じようにbuild.sbtには3.5.2を指定する必要が有ります。バージョンが不一致の場合コンパイルエラーになります。この辺りはドキュメントには書かれていないので、必ずScalatra本体のDependencies.scalaを参照して下さい。

Scalatra 2.6.3で、Jacksonパーサーを使う場合のbuild.sbtは以下のようになります。

val ScalatraVersion = "2.6.3"

libraryDependencies ++= Seq(
  "org.scalatra" %% "scalatra" % ScalatraVersion,
  "org.scalatra" %% "scalatra-json" % ScalatraVersion,
  "org.json4s" %% "json4s-jackson" % "3.5.2",
...
)

これで準備ができました。

JSONを受け取る

HTTP RequestにJSONが含まれている場合、parsedBodyというメソッドを使うとrequestにJSONが含まれているか否かの判定と、JSONのパースまでを一気にやってくれるので便利です。

parsedBodyの使い方

先ほどの環境設定でパーサにJacksonとNativeのうち、どちらを利用するか選択しましたが、選択したパーサに合わせて、JacksonJsonSupport trait又はNativeJsonSupport traitをmix-inします。

例えばJacksonJsonSupportを使う場合は、以下のようなコードになります。

jsonFormatsは、json4sのパーサが必要とする変換方法の指定です。

package com.example.app

import org.scalatra._
import org.scalatra.json._

import org.json4s._
import org.json4s.{DefaultFormats, Formats}

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/") {
    val ast = parsedBody

    ...
  }

parsedBodyContent-TypeJSONの場合にのみパースを行うので、もしJSON以外が指定されている場合はJNothing(Json4sでのNone型に相当)が返ります。また、正常にパースできない、つまりinvalidなJSONデータの場合もJNothingが返ります。

そのため、JNothingが返却される可能性について対応するコードを用意しておく必要が有ります(この肝心なことが公式ドキュメントに書かれていない!!)

ちなみに、Json4sはパースエラー時には例外を送出しますが、Scalatra内でその例外をキャッチしエラーログへ出力し、JNothingを返しているので、Webアプリケーション側には例外は返ってきません。パースエラーになった原因はログから追跡しましょう。

case classにマッピングする

parsedBodyはJson4sのASTを返すメソッドなので、ASTが得られれば後は通常のJson4sの使い方と同じようにJSONデータをcase class等にマッピングして使用します。

case class Person(id: Int, name: String)

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody

    val person = ast.extract[Person]

    person.name
  }

case classにマッピングできない場合は例外が送出されますので、やはり例外に対応したコードを書いておく必要が有ります。ここは素のJson4sなので、Json4sのドキュメントを参照し、色々と試してみて下さい。

またJson4sのcase classへのマッピングはリフレクションを使用していますが、リフレクションの制約によりクラス内で定義されたcase classでは正しく動作しません。必ずトップレベルでcase classを定義するようにして下さい。小さなアプリケーションではうっかりServlet Classの中でcase classを定義してしまいそうになりますが、それは誤りです(初めて使ったとき、これが分からず数時間悩みました)。

下記のissueが参考になるでしょう。

https://github.com/json4s/json4s/issues/125

JsonValueReader

パースしたASTを簡易にサーチするためのヘルパークラスとしてJsonValueReaderというクラスが用意されています。パスをドットで連結したものをreadメソッドに渡すと、ASTを辿って行って、オブジェクトを取得してくれます。

case class Person(id: Int, name: String)
case class Group(name: String, person: Person)

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody
    val reader = new JsonValueReader(ast)
    val name = reader.read("person.name")

    name
  }
}

ドキュメントもテストもないので、これを使うくらいだったらJson4sが提供するクエリ構文を使った方がお勧めです。

XML

Json4sはJSONXMLの相互変換をサポートしているため、scalatra-jsonでもXMLをサポートしています。XMLをリクエストで受け取った場合は、前述の流れと同じようにJson4sのJSON ASTを得ることができます(Content-Typeがxml、つまり'application/xml'でもparsedBodyは有効です)。

ですが、Json4sを使わなくても…という感じなので、そこは普通にXMLとしてパースした方が良いでしょう。

Content-Typeの指定

parsedBodyはContent-Typeヘッダを元にJSONか否かを判定しています(具体的にはapplication/json)。そのため、例えばContent-Typeにapplication/x-www-form-urlencodedが指定されていると、body部をJSONとしてパースはしません(判別できないので当たり前ですが…)。更にServletの仕様により、bodyの内容がパラメータに格納されているパターンも有るので要注意です。

blog.magnolia.tech

クライアントからのContent-Typeに注意しましょう(特にcurlはPOST時はapplication/x-www-form-urlencodedがデフォルトです)。

おわりに

長くなってきたので、ここまで。次回はJSONのレスポンスを生成する箇所をやります。

ScalatraのJSONサポートはJson4sに強く依存し過ぎていて(XMLサポートとか必要?)、昨今の「リフレクションを使っているJson4sを避けた方が良い」という流れも有って、別のJSONモジュールをベースに作り直した方が良い、という時期に来ています(Scalatra-Json2?)。

JSONレスポンスの解説が終わったら、ScalaJsonモジュール一覧の紹介をやろうと思います。

Scalatra in Action

Scalatra in Action

  • 作者: Ivan Porto Carrero,Ross A. Baker,Dave Hrycyszyn,Stefan Ollinger,Jared Armstrong
  • 出版社/メーカー: Manning Pubns Co
  • 発売日: 2014/01/28
  • メディア: ペーパーバック
  • この商品を含むブログを見る

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

「テスト駆動開発」を読んで2018年における開発手法へのスタンスへ思いを馳せる

半年前に購入した「テスト駆動開発」を読み直した話

テスト駆動開発

テスト駆動開発

昨年、新訳版が出版された直後に購入し、Scalaで写経し始めたけど、結局「JavaのコードをどうやってScalaに落とし込むか?」ということばかりを考えてしまい、度々本書で触れられている「歩幅の調整」「仮実装」「明白な実装」「三角測量」等の概念をきちんと理解しないままただコードを書いている状態状態になってしまった。また、その後に「Javaでそのまま写経すればどうか」と思ったけれど、今度はいつの間にか、ただ写経だけに専念している自分に気がつき途中で止めてしまった。

この手の概念を理解するための技術書は、流し読みして分かったつもりになってしまうのが一番怖いので、コードを考えながら書く、都度読書メモを書くなど、自分自身の中に「引っかかりポイント」を作っておかないと、せっかく時間をかけて読み終わってもキーワードしか頭に残らず、「キーワード言いたいだけオジサン」が爆誕してしまうので、「手を動かすこと」「考えること」「結果をアウトプットすること」の3つに気をつけながら読み進めることにした。

と言うわけで、今回は3度目の正直の読書記録…本書は全部で320ページと技術書としては決して分量が多い訳では無いですが、プログラミング自体の入門書やリファレンスではなく、コード例を元に概念を学ぶことが目的の本なので、全ページを同じ密度で読み進めるのはなかなか大変です。

なぜ2018年に「テスト駆動開発」を読むべきか?

原著者のKent Beckによる「Test Driven Development: By Example」が出版されたのが2002年、日本で「テスト駆動開発入門」というタイトルで和訳が出版されたのが2003年と、既に出版から15年が経過しています。以降、原著自体は改版されていませんが、一旦日本語版が2013年に絶版になった後、昨年和田卓人さんによる新訳「テスト駆動開発」が出版されました。新訳にあたっては全編JUnit5とPython3に対応するようにコードが書き直されているほか、後述する訳者による「付録C」が追加されている、という変化点は有るものの、本質的には2002年にリリースされたオリジナルの内容から変わっているわけではありません。

この変化の激しいプログラミングの世界で、2002年に出版された本を2018年に読む意味が有るか?というと、それは確実に有るな、と改めて全ページ読み直して思いました。テスト駆動開発までいかなくてもテストコードを書くことは充分に当たり前になってきましたし、プログラミング言語やライブラリの進化、gitやビルドツール、CIツール等の開発をサポートするツールチェーンの進化、アジャイル開発手法の普及など取り巻く環境が大きく変化しました。

更にはそれらが普及することで、「1つの言語や、開発手法、ツールを導入すれば課題が全て解決する」なんていう都合の良い"銀の弾丸"は無いことをみんなが理解してきたことも大きいのではないかと思っています。

現代のソフトウェア開発は複雑化していて、目の前の課題に対して色々な手法やツールを目的と状況に合わせて組み合わせるしかなく、絶対解唯一解は無いと分かっている状況の中、1つの選択肢として"テスト駆動開発"を理解することは非常に重要で、その意味でもこの本は全然古くなっていないと言えるでしょう。

「付録C 訳者解説:テスト駆動開発の現在」

新訳された「テスト駆動開発」をどこから読めばいいかと言われれば、まずは訳者の和田卓人さんによる「付録C 訳者解説:テスト駆動開発の現在」から読み進めた方が良いでしょう。"テスト駆動開発"という手法を取り巻く歴史、特にRuby on Railsの作者であるDHH(David Heinemeier Hansson)の「TDD is dead. Long live testing.」というブログエントリを出発点として始まった議論について詳細に書かれていて、2017年時点での"テスト駆動開発"の置かれた状況、向き合い方がよく理解できる内容になっています。

「付録C」の最後にこう書かれています。

優れたプログラミングテクニックパラダイムは「使いすぎてみて」少し戻ってくるくらいが良い塩梅です。

「過ぎたるは猶及ばざるが如し」と言いますが、それでも一度ハマってみて一歩引いて俯瞰する姿勢が正しい技術に対するスタンスなのかもしれません。この辺りのスタンスを念頭に置いておく意味でもまずは付録Cを読むことをお勧めします。

一方で、15年前の、出版当時の環境を追体験するために、いきなり冒頭から読み進め、ガツっとハマって、”テスト駆動開発最高!これで全部解決!ロックだ!"みたいな所に思考を一旦持って行って、最後に付録Cでクールダウンするのも良いですね、情熱重要です :)

おわりに

テスト駆動開発」訳者の和田さんに「いいね」押してもらえたこのツイートが自分なりの「テスト駆動開発」に対する感想・解釈の全てかも(誤字があるけど)。

あと、TDDに特化していなくても、テスティングフレームワークにはさまざまソフトウェア開発の知見が詰まっているので、それはそれで色んなテスティングフレームワークのソースを見るのは良い習慣だと思っている。TDDではないけど、PerlTest::Moreと、Test::Harnessソースコードを延々と読むことでPerlについての深い理解を得られたと思っている。

テスト駆動開発」の第Ⅱ部は本当にさらっとだけxUnit系テスティングフレームワークの構造の触りだけを紹介しているだけなので、あれを出発点に実際の色々なソースを読むと更に深い学びが有るので、お勧めです。

「ベタープログラマ」を読んだ

原著が出てたときから割と気になっていた「ベタープログラマ」を読んだ。

全体的な感想

第Ⅰ部はコードスタイルや、不要なコードの存在、テストコードを書く話など、非常に実践的な内容が多かった。

第Ⅱ部は割と考え方というか、思想的な話になっていって、第Ⅰ部をきちんと読んで危機感を持って行動を変えられる人であれば自然とそこに到達するのでは?と思った。

まずは第Ⅰ部をしっかり読んで、自分の置かれた環境との差異や、これから行動することを書き出す、みたいな読み方をすると良い。

第Ⅲ部以降は、もう完全に生き方というか、エンジニアとしての振るまいや、哲学の話になってくるので、一気に通読する、というより少し間を置いて拾い読みしながら読み進めて行くと良いかも。

流し読みしても全然役に立たないタイプの内容なので、読書メモは必ず書いた方がいいと思うし、書かれていることが万人にとって正解、といった類いのものでもないので、その辺はあまり感化されすぎず、「考えるきっかけ」くらいに捉えた方がいい。

第Ⅰ部「you.write(code)」について

冒頭にも書いた通り、第Ⅰ部は実践的な内容が多い。

中でも冒頭の4つの章はお勧め。

  • 2章 見かけのよい状態を維持する
  • 3章 少ないコードを書く
  • 4章 取り除くことでコードを改善する
  • 5章 コードベースの過去の幽霊

2章はコードスタイルの話、ここは1つ1つについて根拠が示されているので、分かりやすい。また、コードスタイルが趣味の話ではなく、コミュニケーションの話であることが明示されていて良い。コードは書く以上に読むもの。

最近はどの言語でもフォーマッタが用意されているので、とにかくフォーマッタを通して一貫性を保つことが良いプラクティスとして広まっているので、どんどん使っていきましょう。個人個人で悩むところではないですね。

3章と、4章はなかなか自然とその考えに至るのは難しい内容、特に3章は分かりづらい、冗長なコードを書いてしまう話なのだけど、実際のコードでは単独のコードだけを見ても分かりづらい話だし、たいていは将来の機能追加に向けた保守性の低下という観点なので、なかなか本質的に理解するのが難しいところ。本の中で言及されているコード例は、かなり分かりやすい事例なので、実際のコードを書くときに、果たしてどこまで意識できるか?というのは難しいかもしれない。

誰も分かりづらく、余計なコードを書こうと思って書く訳では無いので、そこには必ず認知の差が有るわけで、その差をどうやって埋めるか?という話までは当然書かれていないので、そこから先はもう教育の話となってくる。

これは悪いコード例と、良いコード例を比較し続けるしかないし、ひょっとしたらIDEがもっと賢くなって等価でもっとシンプルなコードにリファクタリングしてくれる方に期待した方がいいのかもしれない。

4章はコードを削除する話。ソフトウェアの機能追加は新しい価値を提供するために行われるため、既存の機能を消すということはなかなか無いけど、それはコードの保守性を低下させ、迅速な機能のリリースを妨げる存在であるため、削除した方が良いということが書かれてる。

5章はイデオムについて触れられている。どんな言語やフレームワークでもイデオムは有って、それを身につけないと「らしくない」書き方となってしまい、保守性を低下させる。

おわりに

誰だって悪いコードをわざわざ書きたい人はいなくて、そこには認知の差異や、組織としての方針(投資の優先順位)が有るだけなので、この本に書かれているような悪い事例が目の前に有ったからといって、一方的に批判するのは良くないし、なぜそんな状況が(無知以外の理由で)存在するのか?と考えることが大事なんじゃなかと思いました。

とはいえ、2章から5章あたりは「言われないと分からない」レベルの話ばかりなので、まずはこの辺をしっかり読むことをお勧めします。

後半はどんどん抽象的な話になってきますが、考えるきっかけとしては良い本だと思います。

思ったよりコード例が出てくる量が少なくて、「あれ?」と思ったけど、原著のタイトルである「Becoming a Better Programmer」を考えると、割とそうね、という感じでした。

ベタープログラマ ―優れたプログラマになるための38の考え方とテクニック

ベタープログラマ ―優れたプログラマになるための38の考え方とテクニック

Becoming a Better Programmer: A Handbook for People Who Care About Code

Becoming a Better Programmer: A Handbook for People Who Care About Code

Test2::Prettyを作るために調べたことと、やったこと

Perl 5.26.xでTest2がコアモジュール化しました

Perl 5.26.xでは、コアモジュールのテスティングフレームワークであるTest::Moreモジュールに大改修が行われ、完全にゼロから書き直されれたTest2モジュールベースのものに入れ替えられています(version 1.3xx以降がTest2ベースです)。

Test2そのものの機能説明や、コアモジュール化の状況は、YAPC::Fukuokaでakiymさんが発表された「新時代のテストフレームワークTest2」というスライドをご参照ください。

akiym.hateblo.jp

Test2では、それまでのTAP(Test Anything Protocol)べったりの内部構造から、テストを1つの「イベント」として捉え、テストイベントのストリームを処理する、という思想に変わっています(事実、初期はTest::Streamという名前で開発が始められていました)。おなじみのTAP形式での出力も、単なるフォーマッタによる1つの表現形式に過ぎません。

コアモジュール化により失ったもの

しかし、一方でTest::Moreに対するモンキーパッチで作られていた特殊なテストモジュールが動作しなくなる、という弊害も有ります。執念のような互換性テストにより、たいていのメジャーなテストモジュールはTest2上で動作しますが、根本的にどうしようもないものも有ります。

その中でも最も有名なのが、tokuhiromさん作のTest::Prettyです。

$ cpanm Test::Pretty
--> Working on Test::Pretty
Fetching http://www.cpan.org/authors/id/T/TO/TOKUHIROM/Test-Pretty-0.32.tar.gz ... OK
Configuring Test-Pretty-0.32 ... OK
Building and testing Test-Pretty-0.32 ... FAIL
! Installing Test::Pretty failed. See /xxxx/xxxxxx/.cpanm/work/1521365294.14600/build.log for details. Retry with --force to force install it.

残念ながらテストが通らずインストールが失敗します。これはTest::MoreのコアであるTest::Builderモジュールを徹底的に置き換えるというTest::Prettyの構造がさすがにTest2ベースのTest::Builderでは対応できなかったためです。

Test2::Formatterモジュール

Test2にはTest2::Formatterという、発生したテストイベントを実際のテスト結果の出力へ変換する仕組みが用意されています。例えば、TAP形式への変換はTest2::Formatter::TAPというモジュールが用意されています。

というわけで、Test::Prettyの出力を再現するTest2::Formatter::Prettyを作ってあげれば良い、ということですね。

まずは最小のTest2::Formatter::Prettyを用意してみましょう。Test::Formatterを継承したモジュールは最低限これだけ有れば動きます。

package Test2::Formatter::Pretty;

use strict;
use warnings;

our $VERSION = 'v0.0.1';

use Test2::Util::HashBase qw{
    no_numbers
};

use parent qw/Test2::Formatter/;

sub hide_buffered { 1 }

sub write {
    my ($self, $e, $num, $f) = @_;

    print "にゃーん\n";
}

1;
use Test::More;

use strict;
use warnings;

pass("success!!");
fail("failure!!");

done_testing;
$ T2_FORMATTER='Pretty' perl -Ilib test01.t
にゃーん
にゃーん
にゃーん
にゃーん
にゃーん
$ perl test01.t
ok 1 - success!!
not ok 2 - failure!!
#   Failed test 'failure!!'
#   at test01.t line 7.
1..2
# Looks like you failed 1 test of 2.

テストが2つ(成功と失敗)、ダイアログメッセージが2つ(テストの失敗の詳細と、テストの失敗数)、テストプラン(1..2)が出力されていることが分かるでしょう。

つまり、writeメソッドの中でテストイベントに応じて出力する内容をひたすら変換し、標準出力か、標準エラー出力へ編集していけばOKということになります。

Test::Prettyの仕様を振り返る

ではここで改めてTest::Prettyの仕様を振り返ってみます。

  • テストが成功すれば「ok」ではなく、「✓」を緑で表示する -> 見やすい
  • テストが失敗すれば「not ok」ではなく、「✖」を赤で表示する -> 見やすい
  • テストをスキップすれば、「skip」を黄色で表示する -> 見やすい
  • サブテスト自体のテスト成否を表示しない -> サブテストの中身を見れば充分なので、表示が余計
  • テスト名を宣言しないと、テストコードの該当行のソースをテスト名として表示する -> 実は意外とこれで充分

環境変数HARNESS_ACTIVEが有効なときの挙動はまた違うんですが、複雑になるので、ここでは割愛します。

Test2::Formatter::Prettyを実装する

というわけで、できました「Test2::Pretty」モジュールです。

github.com

はっきり言ってTest2::Formatter::TAPと、Test::Prettyからのコピペのキメラなので、詳しくはソースコードを見てください、としか言い様はありませんが、Test2モジュールがイベントモデルとして綺麗に整理されているので、実装量は非常に少ないです。一部サブテスト周りで表示をスキップするために汚いコードが有りますが、順次改善していきたいと思いますので、使ってみてどんどん感想をください。issue、PRも歓迎です。落ち着いたら、cpanにアップします。

おわりに

長年ウォッチしていたTest2がついにコアモジュール化して、感慨深いものが有ります。 ただ、最近すっかり追いかけていなくて、今回久しぶりにコードを読んでみると全然別物のようになっていたので、追いかけるのに非常に苦労したし、意外とドキュメントも未整備なところが有ったので、これからまたコントリビュートしたい、と思いました。

Test2::Pretty、ぜひ使ってみて下さい。

初めてのPerl 第7版

初めてのPerl 第7版

続・初めてのPerl 改訂第2版

続・初めてのPerl 改訂第2版