2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SATySFiでテストを書きたい

Last updated at Posted at 2022-12-14

SATySFi Advent Calendar 2022 14日目の記事です。

SATySFi でもテストしたいよなーと思って2年くらい前につくりかけで放置しちゃってる(えっもうそんなに経ってるんだ、放置しててごめん……) satysfi-test というライブラリがあります。これをネタにして SATySFi でテストを書くときの諸々についてちょっと書こうかなという記事です。

注意: satysfi-test は開発中のライブラリなので一般に使用できるレベルにはなっていません!

何をテストするか

SATySFiもプログラミング言語なんだから当然テストは書きたいですよね。でもSATySFiで書くテストって具体的にどんなものになるんでしょうか。ちょっと考えてみると以下のように「どのような出力をテストするか」で二つに分類できるはずだという考えに至ります。

  1. inline-boxes, block-boxes, math, document などの組版のためのデータ
  2. int, string, レコード型などの組版に直接関与しないデータ

SATySFi が組版システムであることを考えると1に対するテストを書きたいところですが、今のところSATySFiではこれら類のデータについて解析をしたり描画結果の比較を行ったりするAPIはないため、少なくともSATySFiでそういうテストを書くのは難しいんじゃないかなーというのが現状です。というわけで今回は主に1、つまり組版とは関係ない部分についてのテストについて話します。 satysfi-test もそのような用途を想定したものになっています(組版結果のテストについては昔書いた「SATySFiの組版結果をテストする」 を参照するといいかもしれない)。

satysfi-test でテストを書く

ユニットテストをどのように記述するかはフレームワークによりかなり差がありますが、satysfi-test では elm-test (公式ドキュメント) のAPIを参考にしました。この選択をした理由としては elm-test が以下の要求を満たしていたからです。

  1. SATySFi はリフレクションや例外が使えない関数型言語で、そのような状況で無理なく実装できるAPIである必要があった。
  2. Jest みたいに describe~it~ とテストを記述したかった(これは個人の趣味です。こういう describe でテストを書くやつってどこが発祥なんでしょうね)。

実は satysfi-base のテストを satysfi-test で書こうとしたやつ(issue)があるので実際どんな感じか見てみましょう(⚠️ これも書きかけで放置してるんだ、すまん……)

@require: test/test
@require: test/expect

@import: ../src/char
@import: ../src/eq
@import: ../src/list-ext
@import: ../src/ord
@import: ../src/string

let string-test-cases = open Test in
  describe `String module` [
    it `check equality of strings` (fun () -> (
      Eq.equal String.eq `abc` `abc`
      && not Eq.equal String.eq `abc` `abd`
      |> Expect.is-true
    ));
    it `concatenate strings` (fun () -> (
      String.concat [`a`; `b`; `c`]
      |> string-same `abc`
      |> Expect.is-true
    ));
    it `check emptiness of string` (fun () -> (
      String.is-empty ` `
      && (not String.is-empty `hi`)
      |> Expect.is-true
    ));
    it `split string with a given delimiter` (fun () -> (
      let delimiter = Char.make `;` in
      `spam;ham;eggs;`
      |> String.split-by delimiter
      |> Eq.equal (List.eq String.eq) [`spam`; `ham`; `eggs`; ` `]
      |> Expect.is-true
    ));
    % and so on ...
  ]

こんな感じで satysfi-test では Jest みたいな感じで describe~ittest でテストケースを書いていきます。具体的なシグネチャは以下のような感じです(src/test.satyg)。

module Test : sig

	val test : string -> (unit -> ExpectResult.t) -> TestCase.t
	val it : string -> (unit -> ExpectResult.t) -> TestCase.t
	val describe : string -> TestCase.t list -> TestCase.t
	val run : TestCase.t -> string

end

TestCase.t がテストケースの集合を表す型です(TestCases.t の方がいいのでは??)。describe~it にせよ test にせよテストの説明と実際のテスト(= ExpectResult.tを返す関数)を渡す必要があります。では ExpectResult.t が何かと言うと以下のようになります(src/expect.satyg)。

module Expect : sig

  val always-pass : ExpectResult.t
  val always-fail : ExpectResult.t

  val is-true : bool -> ExpectResult.t
  val is-false : bool -> ExpectResult.t

end

今のところは boolean に関する assertion だけです。シンプルですね~

テストを実行するためには Test.run を使います。例えば satysfi-base のテストのトップレベルのファイルはこうなっています(ソース)。

@require: test/test
@require: test/expect

@import: ../src/eq
@import: ../src/list-ext
@import: ../src/ref
@import: ../src/string

@import: array.test
@import: int.test
@import: list.test
@import: ref.test
@import: regex.test
@import: string.test

open Test in
describe `base` [
  ref-test-cases;
  string-test-cases;
  list-test-cases;
  array-test-cases;
  regex-test-cases;
  int-test-cases;
]
|> run

トップレベルのファイルが document でなくて string を返す(Test.run のシグネチャを見よ!)のはテキストモードを想定しているからです。今のところ組版関係の機能をテストできないのでテキストモードで十分なんですね(pdfを生成したくないですし)。というわけで以下のコマンドでテストを実行できます。

satysfi --text-mode "text" main.test.saty -o report.txt

Test report はこんな感じで出力できます。それっぽいね!

Test summary : 48 run, 48 success, 0 fail

Test run report
- base
  - Ref module
    - make 1 contains 1 -> PASS
    - set overrides the value of ref -> PASS
    - swap two refs -> PASS
    - set temporarily value of ref -> PASS
  - String module
    - check equality of strings -> PASS
    - concatenate strings -> PASS
    - check emptiness of string -> PASS
    - split string with a given delimiter -> PASS
    - convert a string to a list of characters -> PASS
    - compare strings with lexicographical order -> PASS
...

余談

ところで、satysfi-test を書いていて面倒くさいなあと思ったのが、例えば |> Eq.equal (List.eq String.eq) [`spam`; `ham`; `eggs`; ` `] のようにわざわざ型に応じた等価演算子を明示的に渡さないといけない点です。これを型に応じて勝手に等価演算子を選んでくれるようにしてくれると便利かなあと思っています。

例えば Haskell の Type Class 的なものがあると Expect.equals のようなアサーションがいい感じに作れるんですよね。 Expect.equals の型を (Eq a, Show a) => a -> a -> ExpectResult.t のようにすると Expect.equals の引数を適当な等価関数で比較した上で失敗した時に適当にstringifyして "expected xxx, but got yyy" みたいなメッセージを自動で生成できるわけです。

こういった機能を実現するにはさっきも挙げたように Haskell でいうtype classesとか、Scala でいう implicit(context) parameters とか、Rust でいう trait とかがあるといいんですよね。SATySFi だとどういう設計にするといいんでしょうね 🤔

余談2

satysfi 0.1.0 は標準でユニットテスト用のライブラリを提供するっぽい?

というわけでこっちを使った方が無難でしょうね。(まあユニットテスト用ライブラリなんていくらあっても困らないので satysfi-test はちゃんと完成させようと思います、いえあんな放置しといてどの口が言うんだという感じですが……)

はーい、というわけで皆さまよい年末を!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?