yoskhdia’s diary

DDDとかプログラミングとかアーキテクチャとか雑多に

ReactiveStreamsのTCKを通すまでの手順(Publisher編)

ReactiveStreamsではSpecificationだけでなく、TCKも公開されています。

reactive-streams-jvm/tck at v1.0.0 · reactive-streams/reactive-streams-jvm · GitHub

前回公開したScalikeJDBC-streamsもこのTCKを通してみました。 その作業記録エントリです。

※Publisherのみです。

TL;DR

github.com

sbtでTestNGプラグインを導入する

ReactiveStreamsのJVM実装では、TCKTestNGで用意されています。 まずは、sbtプロジェクトから、これを実行できるようにします。 TestNGプラグインが用意されているので、これを使います。

github.com

plugins.sbt

addSbtPlugin("de.johoop" % "sbt-testng-plugin" % "3.0.3")

build.sbt

import de.johoop.testngplugin.TestNGPlugin._

testNGSettings
testNGVersion := "6.10"
testNGSuites := Seq(((resourceDirectory in Test).value / "testng.xml").absolutePath)

sbt-testng 3.0.3時点のデフォルトで使用するTestNGのバージョンは6.9.13.6になっていますが、これだとレポートの出力時にエラーになることがあるので6.10を使うように指定しておきます。 また、TestNGではTest Suiteを外部ファイルで指定するようなのですが、デフォルトはtestng.yamlになっていてDTDのサポートを受けられない点が逆に面倒だったのでXMLを使うよう変更しています。

TCKを書く

TCKもライブラリとして提供されているので、これをlibraryDependenciesに追加します。 バージョンは使用しているReactiveStreamsライブラリのバージョンと合わせておきます。

build.sbt

libraryDependencies += "org.reactivestreams" % "reactive-streams-tck" %"1.0.0" % "test"

テストコードは、ReactiveStreams TCKで提供されているPublisherVerificationを使用します。 TestNGJavaのライブラリですが、Scalaでテストを書いていけます。例えば、こんな感じです。

import org.reactivestreams.Publisher
import org.reactivestreams.tck.{ PublisherVerification, TestEnvironment }
import org.testng.annotations.{ AfterClass, BeforeClass }

class DatabasePublisherTckTest extends PublisherVerification[Any](DatabasePublisherTckTest.environment) {

  @BeforeClass
  def beforeClass(): Unit = {
    // ...
  }

  @AfterClass
  def afterClass(): Unit = {
    // ...
  }

  override def createPublisher(elements: Long): Publisher[Any] = {
    // ...
  }

  override def createFailedPublisher(): Publisher[Any] = {
    // ...
  }
}

object DatabasePublisherTckTest {
  val environment = new TestEnvironment(150L, false)   // タイムアウトまでのmsの指定とデバッグ出力を有効にするかの指定
}

初期化や終了処理があればorg.testng.annotations.{ AfterClass, BeforeClass }などを使用します。 PublisherVerificationで要求されるのは、createPublishercreateFailedPublisherの実装です。

createPublisher

引数elementsで要求された数の要素を持つPublisherを生成して返します。 この時、Long.MaxValueが指定される場合がありますが、その場合は無限の要素を持つPublisherとして返す必要があります。
ScalikeJDBC-streamsでは有限のレコードのみをサポートするため、Long.MaxValueが指定された場合はSkipExceptionを投げるようにしています。*1*2

createFailedPublisher

Spec Rule 1.9に則って、onSubscribeを呼び出した後のタイミングでonErrorを呼び出すPublisherを生成して返します。 テストする必要がなければ null を返します。

注意)Specificationすべてがテストされるわけではない

Structure of the TCKにも書かれていますが、すべてのSpecificationを満たすテストが行われるわけではありません。 いくつかのSpecは意味のある自動テストを行うことが難しいようです。

テストケースの命名規則

ReactiveStreams TCKでは命名規則も定められています。
required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElementsのように、テストの種類_対応するSpec番号_説明とします。 上記のようにPublisherVerificationを使用する場合でも、テスト実行時のレポートを確認する際にこの命名規則を覚えておくと、どのSpecを満たしていないのか分かりやすいです。

TCKを実行する

testng.xmlは先のtestNGSuitesに設定したディレクトリの下に作成します。classタグにテストしたいクラスを記述します。 ScalikeJDBC-streamsでは、こんな風に用意しました。

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="ReactiveStreamsTCK" verbose="2" >
    <test name="DatabasePublisher">
        <classes>
            <class name="scalikejdbc.streams.DatabasePublisherTckTest" />
        </classes>
    </test>
</suite>

これで、いつもどおりsbt testTestNGが実行されます。 TCKで提供される実装はTest Isolationであるためパラレルで実行してもOKのようです。

終わりに

ReactiveStreams TCKを利用すると簡単にSpecificationを満たしているか確認することができます。 ただ、テストはパスしているものの、出力されるログを見ながらこの実装で良いのだろうか…と再確認することもあったので、SpecificationとTCK実装の両方を随時確認しながら対応した方が良さそうでした。

*1:とはいえ、PublisherVerificationの中でLong.MaxValueが渡されてくることはv1.0.0時点では無いです。

*2:Integer.MaxValueは渡されてくるものの、ScalikeJDBC-streamsではDBにこれだけの要素を用意するのも結構つらいので当該Specの実装を確認して省略してしまいました…。