.NET Core用のテストランナーを作る

[2017/02/18追記].NET Core SDK RC以降で実装方法が変わったのでこの記事を読むべきではありません

これは.NET Core Advent Calendar 4日目の記事です。

qiita.com

.NET Coreに対応したユニットテスト

さて、世の中にはすでに.NET Coreに対応済みのテスティングフレームワークがいくつか存在します。

ここで、.NET Core CLI用のランナーを提供しているプロジェクトをみてみましょう。

主要フレームワークはすでにpreviewやbeta版がリリースされているようですね。 Visual Studioにべったり(?)だったMSTestも次のバージョンからNuGetで取得できるようになったのが見所かもしれません。

ちなみに、私もF#向けテスティングフレームワーク開発に携わっている一人としてrunnerを作っていたりします。

今回はこういったランナーを実装するにはどうすれば良いか、という話を雑に書いていきます。

注意事項

  • 結局個人メモみたいな形になってしまったのでわかりづらいかも…
  • 2016/12/04時点での情報なので、正式リリースの際には仕様が変わっているかもしれないことに注意してください。
  • コンソールのみで動くランナーを作る場合にはここまでの知識は必要ありません
    • 逆を言えば、Visual Studio上でテストを動かしたいなら対応必須です

test communication protocol

.NET Core CLIにはtest communication protocolというテスト用のプロトコルが定められています。 Visual Studio 2017以降のテストエクスローラーはこのプロトコルを使って.NET Coreなテストを実行しているようですね。

.NET Core CLI test communication protocol | Microsoft Docs

機械翻訳?もあるみたいです。

.NET Core CLI テスト通信プロトコル | Microsoft Docs

必要になる知識はこれを読めば全部手に入るのですが、これを読んだだけで実装できたら苦労はしない…。

というわけで、以降では実装に必要なものなどをあげていきます。

パッケージ名

パッケージ名に関してはproject.json時代の話を書くので、状況次第では役に立たなくなるかもしれません。

パッケージ名はdotnet-test-foofoo部分にプロジェクト名をつけます。 これはproject.json"testRunner": "foo"と記述すると、.NET CLIが依存関係の中からdotnet-test-fooパッケージを探索するという決まり事があるためです。 先に紹介したドキュメントのシーケンス図内ではdotnet-test-runnerという表現がなされています。

Microsoft.Extensions.Testing.Abstractions

作成したプロジェクトの依存関係に次のパッケージを追加します。

www.nuget.org

このパッケージにはAdapterやdotnet testとの通信で使う型やインターフェースが定義されています。

コマンドラインオプション

ここに列挙するオプションはAdapter, dotnet test, ランナーが連携する際に必要となります。

Test discovery(テスト探索)とTest execution(テスト実行)に共通するオプションとそうでないオプションがあります。

共通

designtimeオプション

design modeを示すオプションです。 このオプションがついている場合はコンソール実行ではないと考えたほうが良いでしょう。

--designtime

portオプション

Adapterとランナー、あるいはdotnet testとランナー間でTCP通信するためのポート番号がオプションで渡されます。

--port ポート番号

テスト対象のアセンブリ一覧

テスト対象のアセンブリはオプション名なしのスペース区切りで渡されます。

testAssembly1 testAssembly2

Test discoveryで必要になるオプション

listオプション

テストのリストアップを要求するオプションです。 このオプションが渡された場合、ランナーはテストを実行する必要はありません。

--list

Test executionで必要になるオプション

wait-commandオプション

実行するテスト一覧を取得するために待機する必要があることを示すオプションです。

--wait-command

実装面

探索したテストの送信やテスト結果の送信はITestDiscoverySinkもしくはITestExecutionSinkを実装したクラスを使って行います。 この2つのインターフェースはMicrosoft.Extensions.Testing.Abstractionsにあります。

ポートが指定されていない場合

Microsoft.Extensions.Testing.Abstractionsに用意されているStreamingTestDiscoverySinkとStreamingTestExecutionSinkを使うのが手っ取り早いです。 おそらくコンソール上で実行しているはずなので、Console.OpenStandardOutputを渡せばよいと思います。

ポートが指定されている場合

独自にITestDiscoverySink, ITestExecutionSinkを実装したクラスを用意します……といっても基本はdotnet-test-nunitのやつをライセンスつけて持って来ればよいと思います。 NUnitのやつはSocket, NetworkStream, BinaryWriterを使って通信するようになっています。 IPアドレスは自分のPCがターゲットなのでIPAddress.Loopback固定で大丈夫なはずです。

Adapterやdotnet testとの通信はMessage型を使ってやりとりされます。 そして、このMessage型のPayloadはNewtonsoft.Jsonを使っているので、送信時は必然的にNewtonsoft.Jsonを使ってシリアライズしてから送信します。

wait-commandが指定された場合

  1. ITestExecutionSink#SendWaitingCommandで待機中であることを送信する
  2. Message#MessageTypeに指定する文字列はTestRunner.WaitingCommand
  3. BinaryReader等で送られてきたJSONをMessageとしてデシリアライズ
  4. ここで送られてくるMessage#Payloadの型はRunTestsMessageなので、message.Payload.ToObject<RunTestsMessage>().Testsなどとすれば実行すべきテスト一覧が取得できる

ここで取得できるテスト一覧はFullyQualifiedNameとなっています。 これを用いてテスト実行前にフィルタリングしていくことになります。

回りくどいなーという気持ちになるかもしれませんが、FullyQualifiedName自体が長かったりそもそもテスト数が多かったりしてコマンドラインのサイズ上限を容易に超えてしまうので仕方がないですね。

メッセージの送信

Adapterやdotnet testに情報を送信する際にもMessage型を使います。

MessageはMessageTypeというstringのプロパティを持ちます。 で、このMessageTypeに指定する文字列は決まっているわけですが、定数として定義されているわけではないのでランナー側で各自定義するしかないようです。 シリアライズ/デシリアライズのことを考えてstringなのはまぁ許せるとして、どうして定数をパッケージ側で用意しなかったのでしょうね…。

TestFound

--listオプションが指定されている時にテストを発見した場合はITestDiscoverySink#SendTestFoundを使ってテストを送信します。 Message#MessageTypeに指定する文字列はTestDiscovery.TestFoundです。

SendTestFoundや後述するSendTestStartedではTest型のオブジェクトを送信する必要があるのですが、Test#Propertiesのvalueにシリアライズできないような値を突っ込むと落ちるので気をつけましょう。

TestStarted

--designtimeオプションが指定されている時、テスト実行前にITestExecutionSink#SendTestStartedを使ってテスト開始を通知します。 Message#MessageTypeに指定する文字列はTestExecution.TestStartedです。

TestResult

--designtimeオプションが指定されている時、テスト実行後にITestExecutionSink#SendTestResultを使ってテスト結果を報告します。 Message#MessageTypeに指定する文字列はTestExecution.TestResultです。

なお、F#のAsyncを使ってParallelに実行しつつITestExecutionSink#SendTestResultを呼び出したところ、テストを2つ以上実行しようとした際にSocketExceptionが投げられました。 同期的な通知に修正したらこの例外は発生しなくなったので、もしかしたら2016/12/04時点ではITestExecutionSink#SendTestResultを同期的に呼び出す必要があるかもしれません(イマイチ確証が持てないですが…)。

後片付け

--designtimeが指定されている場合は、ITestDiscoverySink#SendTestCompletedITestExecutionSink#SendTestComletedを使って完了を通知します。 Message#MessageTypeに指定する文字列はTestRunner.TestCompletedです。

コンソールで実行している場合は別途コンソール用の出力を実装したり、XMLレポートをちゅつ力してあげれば良いでしょう。

おわりに

テストランナーを実装するには色々とお作法に従う必要があることが理解できたかと思います。

しかし、Visual Studioのテストエクスプローラー用拡張を実装するよりは格段に楽になっています。 VS拡張ではドキュメントがほとんどなく、Microsoft.VisualStudio.TestPlatform.ObjectModelというNuGetにpublishされていないdllと格闘する必要があったためです。

今回言及していない点としては、AppDomainどうするの問題があります。 現行の.NET CoreにはAppDomainに相当する機能がないため、どうすれば良いのかよくわからないというのが正直なところです。 xUnit.NETではAppDomainがある環境とない環境でifdefを駆使してるみたいですが、果たしてこれが正解なのか…?

とまぁ、長くなりましたが、実装するかどうかはともかく自分たちが使っているプロトコルの仕様を読んでみるのも一興かと思います。

scalaでmuscle assert的なライブラリの試作

Scaladogというテスティングフレームワークを以前から作ってみているわけですが、こいつでPersimmon.MuscleAssertみたいなことはできるのかなと思い作……ろうとして放置していたものを少し手直しして動くようにしました。

https://github.com/scala-kennel/hound

Diffの実装自体はただのshapelessのようだ。

初期はhttps://github.com/xdotai/diffがよさそうに見えたのでこのライブラリを使っていたのですが、所々使いづらかったりあまり更新頻度高くなさそうだったりScala.js対応されそうになかったりしたので「Diffくらい作れるでしょ」という軽い気持ちで自作に走りました。

なお実装が雑なのでcase classの型が異なる時に型名しか表示されないです。 あと、shapelessを使いサボった関係で、そもそもprimitive type + case classくらいしか表示できない問題があります……Scalaでリフレクションを頑張る体力がないです……。

Stringの個別diffはライブラリがぱっと探せなかったので非対応です。 よさげなライブラリがあれば教えて下さい(Javaのものでも構いません)。

assertion時にオブジェクトのdiffを取得するのは、たぶん有名どころのテスティングフレームワークでも実装できると思います。 が、まぁ需要はなさそうですし私は作る予定はないです。 ScalaTestにはpower assertあるので、基本はそっちで満足する人が多いんじゃないかなぁと。

対象をJSONに特化させるならdiffson等を使って実装する、という手もあります。

最小のコンピュテーション式

メモ。

使う規則

T(e;, V, C, q) = C(e;b.Zero())

この規則がvalidなコンピュテーション式を作れるはず。 Zeroメソッドのみを用意すれば良いのでBuilderの実装も最小限なはず?*1

コード

// 定義
type A() = member x.Zero()=()
let a = A()

// 実際に試す
a { () }

F# 4.0のfsiで確認。

  • ビルダーのクラス名に制限はない
  • 束縛名にも制限はない

なので一文字でも問題なく判別可能です。

公開後の追記

単にビルダーインスタンスを返すだけなのでこっちが正しいですね。 自分でxにしておきながら気付いてなかったです…失礼しました。

zeclさん、指摘ありがとうございました。

番外編

バッククオート2つで囲めばIdentifierにできるので、(たぶん)絵文字も可能です。

type ``🍣``() = member x.Zero()=()
let ``🍣`` = ``🍣``()

``🍣`` { () }

表示されない場合は環境の問題でしょう。

*1:と思っていたらミスしてました。追記部分を読んでください

.NET Fringe Japan 2016の個人的な振り返り

dotnetfringe-japan.connpass.com

運営及び発表者として参加しました。

運営といっても、勉強会運営に慣れている方が多かったのでやること・やらないことが初期から明確だった気がします。 当日の懇親会周りの運営を手伝えなかったのは申し訳なく思いつつ……。

発表内容

not_fsharp_deep_dive.md · GitHub

発表内容はぎりぎりまで悩みつつ、気がつけば難産だったらしいkekyoさんチキンレースを繰り広げていました。

最初はコンピュテーション式のコンパイラ側の話にしようと考えていましたが、朝からそんな話を聞かされるのはつらいだろうと判断して早々に却下。 次にFSharp.Compiler.Serviceに挑戦しようかな、と考えましたが、別の発表でRoslynがでてきそうな気配があったのでこれも見送り。 では自分らしくどういうライブラリを作ったかについて話すか……と思いつつ、いやいやnueccさんの発表があるからなぁ、みたいな。

そうした紆余曲折があった結果、F# がどういう機能を持ち、持たず、その上で具体的に何が作れるのかについてしゃべることにしました。 が、今考えるとFSDNの話は削ってF#とC#関係について話した方が良かった感ありますね。

F#は今の所、.NET Frameworkや.NET Core、C#とは縁を切れない関係です。 おそらく今後もF#は何かしらの影響を与え、そして与えられる立場であることでしょう。 そういう意味で、F#のRFC機能について紹介しました。 RFCはディープではないはず……と思っていたけど、今考えたら「策定段階の企画書読む」って十分ディープでしたね……。

あと、発表の途中で".NET Framework 2.0ではTaskの独自実装が〜"と口走ったのは完全に間違いで、正しくはCancellationTokenSourceです。 大変失礼しました。

全体

今回の登壇者が同じ場所に揃う機会、ないのではないでしょうか。 Partitionの話とか、なかなか聞けないので面白かったです(なお理解度は怪しいorz)。 あとは、本当にマルチプラットフォームだなーという謎の気持ちが大きかったですね。

時間が長丁場だったのは反省点か……とはいえ、もう少し大きな会場で2部屋使えれば解決できるとは思いますが。 そういう意味では、継続するには協力者を増やす必要がありますね。 まぁ、来年のことは来年考えましょう(とか言っているとすぐ来年になる)。

終わりに

異端と呼ばれているイベントに登壇する機会をいただけて、かなり満足しました。

ueberauth_qiitaとueberauth_hatenaを作った

久々にElixirネタ。

といっても表題がすべてを表していますが……。

https://github.com/pocketberserker/ueberauth_qiita

https://github.com/pocketberserker/ueberauth_hatena

ueberauthはElixir向けの認証ライブラリで、RubyのOmniauthに強い影響を受けているらしいです。 TwitterFacebookなど主要なサービスは一通り実装されているので結構便利です。

とはいえ、さすがに日本向けサービスの実装はなかったので、仕組みを調べるついでにQiitaとはてな用のものを作りました。

ueberauth_qiitaはhexにpublish済みですが、ueberauth_hatenaはとある依存ライブラリをscm形式で依存させている関係でpublishできていません (昔はそれでもhexにpublishできていたのだが、仕様が変わった?)

基本的にはueberauth_facebookやueberauth_twitterと同様の実装になっています。 まぁ、サービスごとに微妙に挙動が異なるので辛い気持ちになりましたが……「もう少し統一感だしてくれー」と叫びたくなりました。

特にこれといった技術的解説点もないので以上。

TypeProviderでFizzBuzzを取得可能な自然数型を生成する

コンパイル時に生成できますね、というだけの話です。

準備: FSharp.TypeProviders.StarterPackのインストール

NuGetからインストールしてください。

注意点としては、ファイルの定義順序がそのままだとコンパイルできない可能性があることでしょうか。

  1. ProvidedTypes.fsi
  2. ProvidedTypes.fs
  3. DebugProvidedTypes.fs

コンパイルできるはず。

型を生成してFizzBuzzを取得できるようにする

https://github.com/pocketberserker/MLStudy/commit/9a0a2795d5f8915cab89a5badf65d3abe17b1cf1

FizzBuzz1から100までと範囲が明確に決まっていますが、それでは面白くないので任意の範囲でとれるようにしましょう。 ProvidedStaticParameterでTypeProvider利用時に引数を渡せるようになるとかなんとか。 定義したProvidedStaticParameter群はProvidedTypeDefinition#DefineStaticParametersに渡すことで登録でき、第2引数で生成の操作を記述できます。

λ式内でやっていることは簡単です。

  1. FizzBuzzを計算し
  2. 自然数用の型を定義し
  3. FizzBuzzの結果を取得するプロパティを設定する(今回はFizzBuzzプロパティ)

使う側ではlet inline dump (value: ^n) = (^n: (member FizzBuzz: string) value) |> printfn "%s"という風に静的に解決された型パラメータを用いてFuzzBuzzを持つ値を出力できるようにします。

足し算がしたい

https://github.com/pocketberserker/MLStudy/commit/aee21c03a3d7e804c48e9c2fc13655c459645e40

数値なのに足し算できないのはおかしい、ということで足し算できるようにします。

  1. 数値と型のマッピングをもっておく
  2. 足し算で得た数値の型が存在するならop_Additionメソッドを定義、そうでないなら何もしない

こう改良することで、生成される範囲の自然数型で足し算ができます。 もちろんFizzBuzzも表示できます。

余談

ここで生成した各自然数型は共通するインターフェースを持ちません。 足し算程度ならインターフェースを用意しなくてもオーバーロードでごり押しできます。

発展

この状態ではリストを取得できません。 HListを用いれば良いはずですが、これに関しては読者の課題とします。

Twitterで流れてきたListReaderコンピュテーション式の解説

先日、Twitterid:n7shi さんが面白いコードを投下していた。

これに対しての私のreplyが以下。

これらのコンピュテーション式はちょっとわかりづらいのでちょっとした解説を試みる。

7shiさん版ListReaderBuilder

7shiさんのコードに少し手を加えたものが以下のコードである。 元コードとの挙動に差はない。

type ListReaderBuilder() =
  member __.Bind(_, f) = function
    | x::xs -> f x xs
    | [] -> ()
  member __.Zero() = fun _ -> ()
let listReader = ListReaderBuilder()
 
let test = listReader {
  let! a = () in printfn "%d" a
  let! a = () in printfn "%d" a
}
 
test [1;2;3]
printfn "---"
test [4]

このコードの出力は以下のようになる。

1
2
---
4

このビルダーで使われる変換規則は以下の通り。

  1. T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
  2. T(do e in ce, V, C, q) = T(ce, V, v.C(e; v), q)
  3. T(e;, V, C, q) = C(e;b.Zero())

testを展開すると以下のようになるだろう(実際はもっと異なるかもしれないが、私はコンパイラではないのでわからない)。

// val test: int list -> unit
let test =
  // let! a = () in ... の展開
  listReader.Bind(
    (),
    fun a ->
      // do printfn "%d" a in ... の展開
      printfn "%d" a
      // let! a = () in ... の展開
      listReader.Bind(
        (),
        fun a ->
          // printfn "%d" a; の展開
          printfn "%d" a
          listReader.Zero()
      )
  )
  • Bindメソッドのシグネチャ'T * ('U -> 'U list -> unit) -> ('U list -> unit)
  • Zeroメソッドのシグネチャunit -> ('a -> unit) ((Bindの型パラメータと異なることを強調したかったのであえて'a表記にしている))
    • つまり、分解され残ったlistはZeroで返されるλ式によって捨てられる
    • testでいうと、2回しかBindを呼んでいないため、残った[3]がZeroで返るλ式に渡される
  • printfn部分でaはintに固定される

ことを考えれば、型に問題はないことに気づくだろう。

考察

面白いコードだが、個人的に気になったことがあった。

let! a = () in ...がなんだか冗長に見えるのだ。 do! ...のほうが短くて見やすい気がする。

いじってみた版

type ListReaderBuilder() =
  member __.Bind(g, f) = function
    | x::xs -> f (g x) xs
    | [] -> ()
  member __.Return(_) = fun _ -> ()
let listReader = ListReaderBuilder()
 
let test = listReader {
  do! printfn "%d"
  do! printfn "%d"
}

このビルダーで使われる変換規則は以下の通り。

  1. T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
  2. T(do! e in ce, V, C, q) = T(let! () = e in ce, V, C, q)
  3. T(do! e;, V, C, q) = T(let! () = src(e) in b.Return(), V, C, q)

同じようにtestを展開してみる。

// val test: int list -> unit
let test =
  // do! printfn "%d" in ... の展開
  listReader.Bind(
    (printfn "%d"),
    fun () ->
      // do! printfn "%d"; の展開
      listReader.Bind(
        (printfn "%d"),
        fun () -> listReader.Return(())
      )
  )
  • Bindメソッドのシグネチャ('T -> 'U) * ('U -> 'T list -> unit) -> ('T list -> unit)
  • Returnメソッドのシグネチャ'a -> ('b -> unit)
    • 最終的に残ったリストはReturnが返すλ式で捨てられる
  • g xprintfn "%d" xが実行され、その戻り値unitfに渡る

こちらも型は問題ないことがわかるだろう。

やりたいことができるか試す

その後の7shiさんとのやり取りで、以下のようなことがやりたかったというコメントを頂いた。

不揃いなデータをコンピュテーション式で処理 - Qiita

改変後のコードでも同様の挙動にできるか試してみよう。

ビルダーとaddPersonシグネチャの順序、コンピュテーション式を書き換える。

// 変更したコードのみ載せています
type ListReaderBuilder() =
  member __.Bind(g, f) = function
    | x::xs -> f (g x) xs
    | [] -> ()
  member __.Return(_) = fun _ -> ()

...

let addPerson data name = if name <> "" then persons.[name] <- data

...

        row |> listReader {
            let! company = id
            do! addPerson (company, "会長")
            do! addPerson (company, "社長")
            do! addPerson (company, "副社長")
            do! addPerson (company, "専務")
        }

...

最初の値は他の場所で使いたいのでidで取得する。 残りはprintfnの時と同じ要領だ。

実際に試してみると同じ結果を返した。

おわりに

結果は同じでも、ビルダーの定義次第でコンピュテーション式の書き方が変わる。 他言語のマクロのように自由ではない限られた変換で何が表現できるのか、疑問は尽きない。