Nim 0.20は事実上Nim 1.0のRC版らしいのでNimのテストコードを書くためのTIPS

Nimのバージョン0.20がリリースされました。
これはNim1.0の事実上RC版らしいです。Nim 1.0のリリースの日が近い、ということです。

https://nim-lang.org/blog/2019/06/06/version-0200-released.html

それは置いといてNimでテストコードを書くことについて整理しました。

# 検証環境

nim 0.19.6

なぜ0.19かというと`choosenim`で0.20.0にしたらnimは動きましたが、nimbleが動かなかったからです・・・。

*追記*

前述のversion0.20告知のページみたら0.20でクラッシュする旨が書いてありました。

> As of 21:02 GMT, we have updated the 0.20.0 release tarballs to fix a critical issue where Nimble would crash with an exception. This means that you might have a broken Nimble build on your system if you updated before this time, the easiest way to fix this is to run choosenim #v0.20.0.

> 21:02 GMT、Nimbleが例外でクラッシュするという重大な問題を修正するため、0.20.0リリースのtarballを更新した。これは、あなたがこの時間より前にアップデートした場合、あなたのシステム上で壊れたNimbleビルドがあるかもしれないことを意味します、これを修正する最も簡単な方法はchoosenim＃v0.20.0を実行することです。

# testamentでテストする

2021/03/13追記

Nim公式で提供してるテストランナーのtestamentがNim 1.0.0から利用可能になりました。
testamentについては書く内容が多くなったので以下にまとめました。

https://qiita.com/jiro4989/items/7ca0e6998e484bdc0a30

# assert系でテストする

## assertとdoAssert

Nimでは値比較をしてtrueでない場合は例外を返す`assert`系のプロシージャやテンプレートがいくつかあります。
そのうちよく使うのは`assert`と`doAssert`です。

https://nim-lang.github.io/Nim/assertions.html#doAssertRaises.t%2Ctypedesc%2Cuntyped

~~ではassertとdoAssertどちらを使うべきか、ですがdoAssertを使いましょう。~~

追記：
assertはコンパイル時に無視できるので開発時用にプロシージャ内に埋め込んでおく、というアプローチが良いでしょう。
本番リリース時は`--assertions:off`オプションをコンパイルすることで本番リリースには含めない、という感じで。

runnableExamplesではdoAssertのみを使うようにしましょう。assertをdoAssertに統一するということを[公式が一度行っています](https://github.com/nim-lang/Nim/issues/8709)。

assertとdoAssertの違いですが、assertはコンパイル時にオプションを与えることでassertでのチェックを無視します。
doAssertはオプションを与えてもassertでのチェックを継続します。

```a.nim
assert(false, "assertで失敗した")
doAssert(false, "doAssertで失敗した")
```

```bash
$ nim c -r a.nim
...省略...
Error: unhandled exception: /tmp/a.nim(1, 7) `false` assertで失敗した [AssertionError]

$ nim c --assertions:off -r a.nim
...省略...
Error: unhandled exception: /tmp/a.nim(2, 9) `false` doAssertで失敗した [AssertionError]
```

ですが、これだけでテストコードを書くのは得策ではありません。
なぜかというとassertのみだとテストを通過してもOK/NGなどのメッセージが出力されないからです。
他にもunittestからテストするときのことを考えるとあまり使うべきではありません（後述）。

~~doAssertでテストをするのは、実行可能バイナリ用のmain処理を書かないモジュールでの
簡易な値チェックなどにとどめたほうがよいと思います。
より細かいテストは後述のunittestを使うべきです。~~

2021/03/13追記: 前述の `testament` の通り、今はunittestよりも`testament`を使うのがおすすめされています。

# unittestでテストする

公式で提供されているユニットテスト用のmoduleです。

## テストをするためのプロジェクト構造

`nimble init`でプロジェクトを作成したとき、自動で`tests`というディレクトリが生成されます。
testsディレクトリ配下に`t`で始まるnim拡張子のファイルが存在すれば、`nimble test`を実行したときに
テストコードとして実行されます。

```
project/
+- tests/
|  `- test1.nim
`- project.nimble
```

## suite/testでテストの目的を表現する

`unittest`モジュールでテストする場合は、まず`unittest`をimportし、
次にテストしたいモジュールをimportあるいはincludeします。
以下のように実装し、`nimble test`とコマンドを実行します。

```tests/test1.nim
import unittest

include テスト対象のモジュール

suite "プロシージャ名":
  test "テストの目的、期待値の説明":
    check procedure1() == expect
```

テストの粒度をどれくらいにするかはケースバイケースです。
僕が一番やるのはプロシージャ単位で`suite`を使用します。

引数を書き換えるプロシージャのテストを行う場合は`setup`で変数を宣言すると便利です。
`setup`は`test`が実行される直前に都度実行されます。

```nim
proc add1(n: var int) =
  n.inc
```

```tests/test1.nim
suite "add1":
  setup:
    echo "setup"
    var n = 1
  test "test1":
    n.add1()
    check n == 2
  test "test2":
    n.add1()
    check n == 2
  test "test3":
    n.add1()
    check n == 2
```

```bash
% nimble test
  Executing task test in /tmp/project/project.nimble
  Verifying dependencies for project@0.1.0
  Compiling /tmp/project/tests/test1.nim (from package project) using c backend
CC: project_test1
[Suite] add1
setup
  [OK] test1
setup
  [OK] test2
setup
  [OK] test3
   Success: Execution finished
   Success: All tests passed
```

## expectで例外をテストする

例外が返ることをテストするときは`expect`を使います。

配列、シーケンスの先頭の要素を返す`first`というプロシージャをテストする例を示します。

```src/util.nim
proc first(n: openArray[int]): int =
  return n[0]
```

このプロシージャでは配列が空の配列だとエラーが発生します。
このときに、例外が返ることを`expect`を使用して以下のようにテストします。

```tests/test1.nim
import unittest
include util

suite "first":
  test "1 2 3 == 1":
    check first([1, 2, 3]) == 1
  test "empty data -> IndexError":
    var empty: seq[int]
    expect IndexError:
      discard first(empty)
```

```bash
% nimble test
  Executing task test in /tmp/project/project.nimble
  Verifying dependencies for project@0.1.0
  Compiling /tmp/project/tests/test1.nim (from package project) using c backend
[Suite] first
  [OK] 1 2 3 == 1
  [OK] empty data -> IndexError
   Success: Execution finished
   Success: All tests passed
```


## バイナリファイルをgitの監視対象から除外

`nimble test`を実行するとバイナリファイルがtestsディレクトリ配下に生成されます。
たとえば`tmain.nim`というファイルが存在したとき`tmain`というバイナリファイルがtestsディレクトリ配下に生成されます。
これはgitで管理したくないので、以下のようなgitignoreを追加します。

Windowsだと多分exeファイルを除外します。
(WindowsPCを持っていない)

```.gitignore
tests/*
!tests/*.*

# Windowsだとこう？
tests/*.exe
```

## 非公開プロシージャのテストとimport/include

例えば`src/main.nim`をテストしたいとき、`tests/tmain.nim`内に以下のようにしてモジュールを読み込むことになります。

```nim
import main

# あるいは

include main
```

どちらを使うべきか、ですが非公開のプロシージャのテストをしたい場合は`include`のほうを使う必要があります。
`import`では非公開プロシージャにアクセスできないためです。

しかしながら、`include`を使う場合も問題があります。
`include`を使うと、読み込んだモジュールの`when isMainModule`ブロックも読み込まれてしまい、
テスト時に実行されてしまうためです。

```src/project.nim 
proc add(x, y: int): int =
  return x + y

when isMainModule:
  echo "Main Module"
  quit 0
```

```tests/test1.nim 
import unittest

include project

suite "add":
  test "1 + 1 == 2":
    check add(1, 1) == 2
  test "0 + 1 == 1":
    check add(0, 1) == 1
  test "-1 + 1 == 0":
    check add(-1, 1) == 0
  test "0 + 0 == 0":
    check add(0, 0) == 0
```

```bash
% nimble test
  Executing task test in /tmp/project/project.nimble
  Verifying dependencies for project@0.1.0
  Compiling /tmp/project/tests/test1.nim (from package project) using c backend
CC: project_test1
Main Module
   Success: Execution finished
   Success: All tests passed
```

`test`のメッセージが表示されていませんので、`quit`でプロセスが終了してしまって
すべてのテストが実行されていません。

これは実行可能ファイルを作る目的のmain処理を書いているモジュールをテストする時問題になります。
`when isMainModule`のブロックの途中や最後に`quit`処理を入れていた場合に、
後続のテストコードが実行されなくなってしまいます。

なので、`include`を使ってテストをする前提のモジュールで`when isMainModule`を書くときは
テストコードから読み込まれても問題ないように実装する必要があります。

### main処理の存在するモジュールのプロシージャをテストする

とはいえ、main処理を書くモジュール内にプロシージャを追加したいケースは普通にあります。
その場合は、面倒ですけれど`include`する前提の非公開プロシージャのみ定義したモジュールを別で作成します。

```
project/
+- src/
|  +- main/
|  |  `- util.nim   <-- これをincludeする
|  `- main.nim
`- tests/
   `- tutil.nim     <-- util.nimのテストコード
```

```main.nim
include main/util

when isMainModule:
  echo add(1, 2)
```

```main/util.nim
# includeする前提なら`*`で公開プロシージャにしなくてよい
proc add(a, b: int): int =
  return a + b
```

### モジュールを分けずにmain処理の存在するモジュールのプロシージャをテストする

2019/09/21 追記

Nimには変数が定義されているかをチェックする`defined`という関数があります。
これと`when`を組み合わせることで、`nimble test`の時だけ`when isMainModule`のブロックを無効化できます。

`nimble build`のときは`-d:flag`でフラグを渡せるのですが、`nimble test`の時はダメっぽかったので、`tests`ディレクトリ配下に`config.nims`という設定ファイルを配置してフラグを渡します。
以下のように書きます。

```tests/config.nims
switch("path", "$projectDir/../src")
switch("d", "isTesting")
```

この状態で、以下のようなファイルを書いて`nimble test`してみます。

```src/main.nim
when isMainModule and not defined(isTesting):
  echo "isMainModule"
  quit 0
echo "not MainModule"
```

```tests/tmain.nim
import unittest

include main

echo "test end"
```

テストの実行

```bash
$ nimble test
... 省略 ...
Hint: /tmp/nimtest/tests/tmain  [Exec]
not MainModule
test end

# もちろんビルドして実行できる
$ nim c -r src/main.nim
... 省略 ...
Hint: operation successful (14193 lines compiled; 0.297 sec total; 15.945MiB peakmem; Debug Build) [SuccessX]
Hint: /tmp/nimtest/src/main  [Exec]
isMainModule
```

通常だとincludeした箇所でquitが実行されて後続のNimコードは実行されず、
メイン処理を実装しているファイル内のプライベートなプロシージャをテストできませんでした。

フラグを利用することで、メイン処理を実装しているファイルのプライベートなプロシージャも
いちいちファイルを分けずにテストできます。

## 参照型のテスト

Nimでは嬉しいことに配列やシーケンス、構造体の値比較が`==`で行えます。
参照型の値比較は`==`では行えないのですが、これも容易に回避する方法があります。
`[]`というプロシージャを使用することで参照型の値の取得が可能です。
これを利用して参照型の値比較も容易に行えます。

https://nim-lang.org/docs/tut1.html#advanced-types-reference-and-pointer-types

一部を抜粋して和訳。

> 空の[]添え字表記は、参照をデリファレンスするために使用できます。つまり、参照が指す項目を取得するという意味です。

```nim
type
  Obj = object
    n: int
  RefObj = ref object
    n: int

echo "Obj:      ", Obj(n: 1) == Obj(n: 1)
echo "RefObj:   ", RefObj(n: 1) == RefObj(n: 1)
echo "RefObj[]: ", RefObj(n: 1)[] == RefObj(n: 1)[]
echo "Array:    ", [1, 2, 3] == [1, 2, 3]
echo "Seq:      ", @[1, 2, 3] == @[1, 2, 3]
```

```bash
% nim c -r b.nim
Obj:      true
RefObj:   false
RefObj[]: true
Array:    true
Seq:      true
```

# runnableExamplesでテストする

これはテストコードを書くためのメインに使うものではないですが、
ドキュメントを書くためには非常に便利です。

Nimでドキュメンテーションコメントを書くときに
runnableExamplesを書くと、書いたサンプルコードが実際に動作することを検証できます。

これは標準ライブラリでは、プロシージャごとのドキュメンテーションコメントを書く際に使用されています。
主な用途はプロシージャの使い方サンプルコードの表現ですが、モジュールのトップレベルのドキュメントを書く際にも使用できます。

しかしながら、runnableExamplesで生成されるドキュメントからは
空白行や余分な空白、コメントが削除されてしまうので、
みやすさを確保したドキュメントを整備したいときには使用できません。
また、コメントを一緒に描画したい場合は`##`でコメントを書かないと消されてしまいます。

```nim
## project はNimのドキュメント生成の練習用のモジュールです。
##
## code-blockで表現する例
## ----------------------
##
## .. code-block:: nim
##
##    import project
##
##    # sum のテスト
##    ## sum のテスト2
##    doAssert sum(@[@[1, 2, 3],
##                   @[4, 5, 6],
##                   @[7, 8, 9]]) == 45
##
## runnableExamplesで表現する例
## ----------------------------
##

runnableExamples:
  import project

  # sum のテスト
  ## sum のテスト2
  doAssert sum(@[@[1, 2, 3],
                 @[4, 5, 6],
                 @[7, 8, 9]]) == 45

import sequtils

proc sum*(n: seq[seq[int]]): int =
  return n.mapIt(it.foldl(a+b)).foldl(a+b)

when isMainModule:
  echo sum(@[@[1, 2], @[3, 4]])
```

このコードから`nim doc`で以下のドキュメントが生成されます。

![2019-06-13-171728_1920x1080_scrot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69665/ca95daeb-5c14-b6b4-0b9d-b751694776ca.png)

runnableExamplesで書いたサンプルコードのほうは、改行が詰められてしまい、
コメントも消えていることがわかります。

runnableExamplesで、コンパイルして実行して正常終了しないコードが存在した場合は
`nim doc`のときに異常終了します。

# まとめ

自分でライブラリを書いたり、[Nimの標準ライブラリのドキュメント整備](https://github.com/nim-lang/Nim/issues/10330)に関わったりした際に
色々ハマって解決したときの情報を整理しました。

自分がハマった情報を全部ここに書き出したので、セクションごとの繋がりがないかもしれないです。
Nimユーザの助けになれば幸いです。