Nim公式テストランナーのtestamentを使って単体テストを行う

# まえがき

- testament というツールの使い方を説明します
- testament 用のテストコードを紹介します

# testament とは

Nim公式のテストランナーです。Nim自体の開発にも使われている実績があります。
testamentはNimをインストールした時に一緒にインストールされます。

testament に関するドキュメントは [testament](https://nim-lang.org/docs/testament.html) に記載されています。

> Testament is an advanced automatic unittests runner for Nim tests, is used
> for the development of Nim itself, offers process isolation for your tests,
> it can generate statistics about test cases, supports multiple targets (C,
> C++, ObjectiveC, JavaScript, etc), simulated Dry-Runs, has logging, can
> generate HTML reports, skip tests from a file, and more, so can be useful to
> run your tests, even the most complex ones.
>
> Testamentは、Nimテスト用の高度な自動単体テストランナーであり、Nim自体の開発に
> 使用され、テストのプロセス分離を提供し、テストケースに関する統計を生成でき、
> 複数のターゲット（C、C ++、ObjectiveC、JavaScriptなど）をサポートします。シミ
> ュレートされたドライラン、ロギング機能、HTMLレポートの生成、ファイルからのテ
> ストのスキップなどが可能なため、最も複雑なテストであっても、テストの実行に役
> 立ちます。

このテストツールの使い方を見ていきます。

# その前に、unittestモジュールではダメなのか？

Nimにはユニットテスト用のモジュールとして[unittest](https://nim-lang.org/docs/unittest.html)が提供されています。
こちらのモジュールを使って単体テストを実施するのではダメなのでしょうか？

unittestの使い方については Nim 0.20 の頃に以下の記事にまとめました。
[Nim 0.20は事実上Nim 1.0のRC版らしいのでNimのテストコードを書くためのTIPS](https://qiita.com/jiro4989/items/d9ddb8a19fbe6ec55d1a)

こちらを使ってもダメなわけではないです。
しかしながら、上記unittestモジュールの説明に以下の一文が記載されていました。

> Note: Instead of unittest.nim, please consider to use the testament tool
> which offers process isolation for your tests. Also when isMainModule:
> doAssert conditionHere is usually a much simpler solution for testing
> purposes.
>
> 注：unittest.nimの代わりに、テストのプロセス分離を提供するtestamentツールの使
> 用を検討してください。また、isMainModuleの場合：doAssert conditionHereは通常、
> テスト目的ではるかに単純なソリューションです。

testamentが利用可能になったのはどうやらNim 1.0.0リリース時からのようです。
[Changelog 1.0.0](https://github.com/nim-lang/Nim/blob/devel/changelogs/changelog_1_0_0.md#tool-changes)

前述の説明からは、将来的なunittestモジュールの削除の可能性については分かりません。
実際unittestモジュールを使用しているサードパーティのライブラリは多数存在します。
いきなりunittestモジュールをdeprecatedにして削除することは考えにくいです。

ですが、公式としてはtestamentを使うことを推奨しているように受け取れます。

これを踏まえて、「testamentを使うテストの書き方」「テス
トの実施方法」「unittestモジュールは使ってはいけないのか」を調査しようと思い至りました。

# testamentを使ってみる

兎にも角にもtestamentを使ってみます。
[testament](https://nim-lang.org/docs/testament.html)に書かれている内容と一部重複します。

## シンプルな単体テスト

まず、一番単純な単体テストのサンプルを示します。
testamentではデフォルトで`tests`ディレクトリ配下のファイル拡張子nimのファイルをテスト対象のコードと判定します。

以下のNimプログラムを作成します。

```tests/sample.nim
doAssert 1 == 1
```

次に、`testament run <ファイル名|モジュール名>`を実行すると、テストが実行されます。
（フルパスでも相対パスでもなく、ファイル名）

```bash
$ ls tests/sample.nim
tests/sample.nim

$ testament run sample.nim
PASS: tests/sample.nim C                                           ( 0.46 sec )
Used /home/jiro4989/.nimble/bin/nim to run the tests. Use --nim to override.
```

また、`testament run` のエイリアスとして `testament r` も存在します。
よって`testament r <ファイル名>` でも同様にテストが実行できます。

## 複数ファイルのテスト

testsディレクトリ配下に複数ファイルが存在する場合は`testament pattern <glob>`を使用します。

```bash
$ ls tests/*.nim
tests/sample.nim  tests/sample2.nim

$ testament pattern 'tests/*.nim'
PASS: tests/sample.nim C                                           ( 0.24 sec )
PASS: tests/sample2.nim C                                          ( 0.43 sec )
Used /home/jiro4989/.nimble/bin/nim to run the tests. Use --nim to override.
```

また、`testament pattern` のエイリアスとして `testament p`と`testament pat`も存在します。

## HTMLレポートの生成

前述のtestamentのドキュメントに記載の通りHTMLレポートが取得できます。
`testament html`と実行すると、カレントディレクトリに`testresults.html`というファイルが生成されます。

```bash
$ testament html
Used /home/jiro4989/.nimble/bin/nim to run the tests. Use --nim to override.

$ ls testresults.html
testresults.html
```

このファイルをブラウザで開いてみると、以下の画面が表示されます。

![testament2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69665/eda7bc1b-3786-d57a-1b00-86c36f107835.png)


見やすく整えられたレポートが確認できました。

# testament用のテストコードを書く

testamentの基本的な使い方がわかったところで、testament用のテストコードを書いて
いきます。testament用のテストコードの書き方については[testament](https://nim-lang.org/docs/testament.html)にもサンプルが載っているので重複する内容があります。

## specを書く

テストファイルごとの、テストする際の設定やテスト期待値、入力などを制御する設定を書きます。
これらの設定をtestamentでは **spec** と呼びます。
specは省略可能です。

specはファイルの先頭に以下のような文字列を定義することで表現します。

```nim
discard """
  exitcode: 0
"""

# 以降はテストコード
doAssert 1 == 1
```

このdiscardしている文字列に定義可能な設定の詳細は[testament](https://nim-lang.org/docs/testament.html)の
「Writing Unittests」を参照してください。

定義可能な設定はたくさん存在しますが、頻繁に使用すると思ったものをピックアップして説明します。

### action

テスト実行時の動作を設定します。

設定可能な値は以下の3つです。

- `compile` コンパイルが成功することを期待します
- `run` コンパイルとプログラムの実行が成功することを期待します
- `reject` コンパイルが失敗することを期待します

```nim
discard """
  action: "run"
"""
```

### exitcode

テストが終了した時の終了コードを設定します。

exitcodeで、テストの成功を期待したい場合は 0 を設定します。
テストの失敗を期待したい場合は 1 を設定します。
デフォルトでは 0 が設定されています。

```nim
discard """
  exitcode: 0
"""
```

### output

テストが終了した時の期待する標準出力を設定します。

outputを設定した場合は、そのテストコードが標準出力に書き出す文字列がoutputと一
致しない場合にテスト失敗と判定されます。

output未設定の場合は、そのテストコードが何を標準出力しても無視されます。

```nim
discard """
  output: '''
hello world
'''
"""

echo "hello world"
```

### input

テストを実行する際に、標準入力に文字列を渡します。

```nim
discard """
  input: '''
hello world
'''
"""

doAssert stdin.readline == "hello world"
```

### errormsg

コンパイルエラーになったときのエラーメッセージを設定します。

以下は`func`を使用してプロシージャを定義しようとするものの、`echo`が副作用を持
っているためコンパイルエラーになる例です。

```nim
discard """
  errormsg: "'sideeffect' can have side effects"
"""

func sideeffect =
  echo 1
```

### cmd

テストの実行に使用するコマンドです。
省略されている場合、あるいは空文字が設定されている場合は、次のように解釈されます。

`"nim $target --hints:on -d:testing --nimblePath:tests/deps $options $file"`

この機能の使い所としては、「特定のテストケースだけ `GC:ARC` や `GC:ORC` を有効
にしたい」や「特定のテストだけフラグをONにしたい」といった場合に役に立つと思い
ます。

以下は `GC:ORC` を有効にし、`debugFlag`という独自のフラグ定数をこのテストのみtrueにする例です。

```nim
discard """
  cmd: "nim c --gc:orc -d:debugFlag -r $file"
"""

const debugFlag {.booldefine.} = false

doAssert debugFlag == true
```

### target

テストの実行ターゲットを指定します。

Nimでは C, C++, JavaScript などがコンパイル時に指定できますが、ここでもその指定が可能です。
複数指定する場合は、半角スペースで区切って入力します。

```target.nim
discard """
  target: "c cpp js"
"""

doAssert true
```

上記設定をした上でtestamentを実行すると、以下のように3つのテストが走っていることが確認できます。

```bash
$ testament pattern 'tests/*.nim'
PASS: tests/target.nim C                                           ( 0.43 sec )
PASS: tests/target.nim C++                                         ( 0.51 sec )
PASS: tests/target.nim JS                                          ( 0.38 sec )
```

## assertionを書く

unittestでは`check`マクロを使ってプロシージャのテストを行っていました。

testamentでは`doAssert`または`unittest.check`, `unittest.require`を使うのが良いようです。
参考: [Best practices](https://nim-lang.github.io/Nim/contributing.html#best-practices)

> Use doAssert (or unittest.check, unittest.require), not assert in all tests so they'll be enabled even with --assertions:off.

ただし`unittest.suite`と`unittest.test`は使ってはいけません。
これら2つを使ってはいけない理由は後述します。

```nim
doAssert 1 + 1 == 2

# または

import unittest
check 1 + 1 == 2
```

僕は`unittest.check`を使うのをオススメします。理由は2つあります。

1. テストがエラーになっても後続のテストが実行される
2. テスト失敗理由がわかりやすい

### unittest.check

以下に`unittest.check`を使うサンプルを用意しました。

```tests/t_unittest.nim
discard """
  output: '''
1
'''
"""

import unittest

echo 1

check 1 == 1
check 1 == 1
check 1 == 1
```

このテストコードに対してtestamentを実行してみると、正常にパスします。

```bash
$ testament r t_unittest
PASS: tests/t_unittest C                                           ( 0.63 sec)
```

次に、このコードをわざと失敗するように修正してみます。

```tests/t_unittest.nim
discard """
  output: '''
1
'''
"""

import unittest

echo 1

check 3 == 1
check 2 == 1
check 1 == 1
```

このテストコードに対してtestamentを実行してみると、エラーになります。
そして、テストが失敗した箇所がすべてメッセージとして出力されます。

```bash
⟩ testament r t_unittest
FAIL: tests/t_unittest C
Test "tests/t_unittest" in category "t_unittest"
Failure: reExitcodesDiffer
Expected:
exitcode: 0

Gotten:
exitcode: 1

Output:
1
/tmp/work/tests/t_unittest.nim(11, 8): Check failed: 3 == 1
/tmp/work/tests/t_unittest.nim(12, 8): Check failed: 2 == 1

FAILURE! total: 1 passed: 0 skipped: 0 failed: 1
```

エラーが発生した箇所がわかりやすくなっています。

### doAssert

`doAssert`を使った場合は以下のようになります。

```tests/erros.nim
doAssert false, "error 1"
doAssert false, "error 2"
doAssert false, "error 3"
```

上記コードをtestamentで実行してみます。

```bash
$ testament r errors
FAIL: tests/errors C
Test "tests/errors" in category "errors"
Failure: reExitcodesDiffer
Expected:
exitcode: 0

Gotten:
exitcode: 1

Output:
/tmp/work/tests/errors.nim(1) errors
/home/jiro4989/.choosenim/toolchains/nim-1.4.4/lib/system/assertions.nim(30) failedAssertImpl
/home/jiro4989/.choosenim/toolchains/nim-1.4.4/lib/system/assertions.nim(23) raiseAssert
/home/jiro4989/.choosenim/toolchains/nim-1.4.4/lib/system/fatal.nim(49) sysFatal
Error: unhandled exception: /tmp/work/tests/errors.nim(1, 10) `false` error 1 [AssertionDefect]

FAILURE! total: 1 passed: 0 skipped: 0 failed: 1
```

Outputには1行目のエラーしか出力されていません。
よって1行目を修正して再度testamentを実行すると、2行目がエラーになります。

このことから、1つのファイル内で複数のassertionを書く場合は
`unittest.check`を使ったほうが複数のエラーを一度に確認できて良いと思っています。

## 例外処理のテストを書く

unittestでは`expect`マクロを使ってプロシージャのException型を判定するテストを書くのが一般的です。

testamentでも同様に`expect`を使ってException型を判定するのが良さそうです。
BestPracticesには特にこういった記載はありませんでしたが、[公式リポジトリではstdlib/tnet.nimがこのように例外テストを行っています。](https://github.com/nim-lang/Nim/blob/devel/tests/stdlib/tnet.nim)

以下のケースではtestamentはPASSします。

```nim
discard """
  output: "1"
"""

import unittest

expect IndexDefect:
  var n = @[0, 1, 2]
  discard n[255]

echo 1
```

以下のように、期待値と異なる例外が返って来た場合はtestamentはエラーになります。

```nim
discard """
  output: "1"
"""

import unittest

expect IndexDefect:
  var n = @[0, 1, 2]
  discard n[255]

expect ValueError: # <-- ここがエラー
  var n = @[0, 1, 2]
  discard n[255]

echo 1
```


## 参考になるテストコード

公式でtestamentを使用していると記載の通り、公式リポジトリ内にtestament用のコードが存在します。
自分でtestamentのコードを書く時は、このディレクトリ配下のコードを大いに参考にしました。

- <https://github.com/nim-lang/Nim/tree/devel/tests>

サードパーティのパッケージでtestamentを使用しているものはあまり見たことないのですが、
Webフレームワークの[Prologue](https://github.com/planety/prologue/tree/devel/tests)がtestamentを使用してテストを行っていました。

# testamentでunittestモジュールは使えないの？

結局のところtestamentはNimのファイルを1つ1つコンパイルして実行して、
specに書かれた設定をもとに標準出力や終了コードを比較してるだけの原始的なテストランナーのようでした。

それだったらunittestモジュールもtestament内で使用できるのでは？と思いました。
前述の`unittest`モジュールの説明では「unittest.nimの代わりに」という記載がありました。
公式ドキュメントのBest practiciesではunitest.checkを使うように書いてありました。
実際のところtestamentでunittestモジュールを使ってはいけないのでしょうか？

これは「使って良い」です。
ただし**「一部」使ってはいけない機能が存在します**。

[Writing tests stdlib](https://nim-lang.github.io/Nim/contributing.html#writing-tests-stdlib)によると「`unittest.suite`と`unittest.test`は使ってはいけない」ようです。

> Don't use `unittest.suite` and `unittest.test`.

その理由は、おそらくですが `unittest.suite` と `unittest.test` が標準出力にメッセージを出力する
ことが原因ではないかと考えています。

というのも、unittest.suiteを使用してもoutputを指定しなければ問題はありませんでした。
以下のtestament用のテストコードは正常に動作します。

```nim
import unittest

suite "suite":
  test "test":
    check true
```

しかしながら、specにoutputを設定した時に問題が発生します。

前述のコードを以下に修正しました。
specに output 1 を追加しただけです。

```nim
discard """
  output: '''
1
'''
"""

import unittest

suite "suite":
  test "test":
    echo 1
    check true
```

この状態でtestamentを実行してみます。
結果は以下のとおりです。

```nim
⟩ testament r t_unittest
FAIL: tests/t_unittest C
Test "tests/t_unittest" in category "t_unittest"
Failure: reOutputsDiffer
Expected:
1

Gotten:
[Suite] suite
1

FAILURE! total: 1 passed: 0 skipped: 0 failed: 1
```

unittest.suiteを使用すると`[Suite]`といったメッセージを標準出力に出力します。
これがtestamentのoutput比較に干渉してしまい、結果的にテストを意図せず破壊してし
まいます。

おそらくこれと相性が悪くて`unittest.suite`と`unittest.test`を使ってはいけない、となっているのでしょう。
逆にいうと、この記事内で記載した`unittest.check`と`unittest.expect`は使っても良いはずです。
試した限りではテストの実施に問題はなさそうでした。

# unittestのみを使うべきか、testamentを使うべきか

前述の通りtestamentを使ってテストを実施する場合はunittestの一部の機能が利用不可能になります。

本音を言うとtestament用のテストコードの書き方はダサいと思ってます。

testamentを使わずにunittestモジュールだけ使ってテストを実装する場合は
suite, test, setup, teardownなどが利用可能です。
これらの機能はテストの共通処理をシンプルに記述できて、スマートにテストコードを実装できます。

小さなプロジェクトではunittestモジュールを使用してテストコードを書いても悪くな
いと思っています。

testamentを使うべき理由をあげるなら、「より細かいテストを実施したい」場合です。

前述の通りtestamentではファイル単位でspecを設定できるため
`unittest` + `nimble test`でのテストよりも、より細かいテストを実施できます。
`nimble test`でのテストではファイル単位でのコンパイル時のバックエンド指定や、フ
ラグの切り替えなどはできないためです。

特定のテストをスキップするといった制御も可能です。

使ったことはないのですが、testamentではspecに`valgrind: true`といったオプションがあり、
特定のテストでのみ`valgrind`を使ってメモリリークを検出したりが可能なようです。

また、Nimは複数のGCをサポートしており、ユーザが任意でGCを選んでコンパイルできます。
これらの機能を細かくテストするのにtestamentは有効なのだと思われます。
まさにNimのためのテストランナーだと感じました。

しかしながら、これらの機能は小さなプロジェクトでは多機能すぎるとも感じています。
プロジェクト規模や複雑さ、提供する機能に応じてtestamentの採用を検討するのが良いのでは、と思います。
（なので`unittest`のドキュメントでも「please consider to use the testament」と記載されているのかもしれません）

他にもtestamentにはメリットがあります。
testamentはHTMLレポート機能を持っており、テストケースごとの処理時間も出力してい
るため、ある程度テスト結果の分析が可能です。

採用されるかは不明ですが、CodeCoverageを取得できるようにするための改修のPRが作成されています。
<https://github.com/nim-lang/Nim/pull/15827>

今後CodeCoverageを取得できるようになった場合、機能として提供されるならおそらく
testamentに追加されるだろうと踏んでいます。

よって、僕は新規にテストを書く時はtestamentを使うつもりです。

# unittestからtestamentに移行してみた

参考程度に、nimbleに登録している自前のパッケージの一つの`envconfig`のテストコードをすべてtestamentに書き直したときのPRを記載します。
（この時点では`unittest.check`を使っても良いことに気づいていなかったので`doAssert`を使っていますが、`unittest.check`を使ったほうが良いと思います)

<https://github.com/jiro4989/envconfig/pull/3>

# まとめ

駆け足な説明でしたが、まとめとしては以下の通りとなります。

- testamentの使い方を説明しました
- testamentでのテストの書き方を説明しました
- testamentでunittestモジュールは使っても良いが、一部のunittestの機能が使えなくなります
- testamentを採用するべきかどうかについての私感を述べました

Nimでのテスト実施の一助となれば幸いです。