Nimのバージョン0.20がリリースされました。
これはNim1.0の事実上RC版らしいです。Nim 1.0のリリースの日が近い、ということです。
それは置いといて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については書く内容が多くなったので以下にまとめました。
assert系でテストする
assertとdoAssert
Nimでは値比較をしてtrueでない場合は例外を返すassert
系のプロシージャやテンプレートがいくつかあります。
そのうちよく使うのはassert
とdoAssert
です。
ではassertとdoAssertどちらを使うべきか、ですがdoAssertを使いましょう。
追記:
assertはコンパイル時に無視できるので開発時用にプロシージャ内に埋め込んでおく、というアプローチが良いでしょう。
本番リリース時は--assertions:off
オプションをコンパイルすることで本番リリースには含めない、という感じで。
runnableExamplesではdoAssertのみを使うようにしましょう。assertをdoAssertに統一するということを公式が一度行っています。
assertとdoAssertの違いですが、assertはコンパイル時にオプションを与えることでassertでのチェックを無視します。
doAssertはオプションを与えてもassertでのチェックを継続します。
assert(false, "assertで失敗した")
doAssert(false, "doAssertで失敗した")
$ 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
とコマンドを実行します。
import unittest
include テスト対象のモジュール
suite "プロシージャ名":
test "テストの目的、期待値の説明":
check procedure1() == expect
テストの粒度をどれくらいにするかはケースバイケースです。
僕が一番やるのはプロシージャ単位でsuite
を使用します。
引数を書き換えるプロシージャのテストを行う場合はsetup
で変数を宣言すると便利です。
setup
はtest
が実行される直前に都度実行されます。
proc add1(n: var int) =
n.inc
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
% 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
というプロシージャをテストする例を示します。
proc first(n: openArray[int]): int =
return n[0]
このプロシージャでは配列が空の配列だとエラーが発生します。
このときに、例外が返ることをexpect
を使用して以下のようにテストします。
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)
% 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を持っていない)
tests/*
!tests/*.*
# Windowsだとこう?
tests/*.exe
非公開プロシージャのテストとimport/include
例えばsrc/main.nim
をテストしたいとき、tests/tmain.nim
内に以下のようにしてモジュールを読み込むことになります。
import main
# あるいは
include main
どちらを使うべきか、ですが非公開のプロシージャのテストをしたい場合はinclude
のほうを使う必要があります。
import
では非公開プロシージャにアクセスできないためです。
しかしながら、include
を使う場合も問題があります。
include
を使うと、読み込んだモジュールのwhen isMainModule
ブロックも読み込まれてしまい、
テスト時に実行されてしまうためです。
proc add(x, y: int): int =
return x + y
when isMainModule:
echo "Main Module"
quit 0
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
% 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のテストコード
include main/util
when isMainModule:
echo add(1, 2)
# 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
という設定ファイルを配置してフラグを渡します。
以下のように書きます。
switch("path", "$projectDir/../src")
switch("d", "isTesting")
この状態で、以下のようなファイルを書いてnimble test
してみます。
when isMainModule and not defined(isTesting):
echo "isMainModule"
quit 0
echo "not MainModule"
import unittest
include main
echo "test end"
テストの実行
$ 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では嬉しいことに配列やシーケンス、構造体の値比較が==
で行えます。
参照型の値比較は==
では行えないのですが、これも容易に回避する方法があります。
[]
というプロシージャを使用することで参照型の値の取得が可能です。
これを利用して参照型の値比較も容易に行えます。
一部を抜粋して和訳。
空の[]添え字表記は、参照をデリファレンスするために使用できます。つまり、参照が指す項目を取得するという意味です。
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]
% nim c -r b.nim
Obj: true
RefObj: false
RefObj[]: true
Array: true
Seq: true
runnableExamplesでテストする
これはテストコードを書くためのメインに使うものではないですが、
ドキュメントを書くためには非常に便利です。
Nimでドキュメンテーションコメントを書くときに
runnableExamplesを書くと、書いたサンプルコードが実際に動作することを検証できます。
これは標準ライブラリでは、プロシージャごとのドキュメンテーションコメントを書く際に使用されています。
主な用途はプロシージャの使い方サンプルコードの表現ですが、モジュールのトップレベルのドキュメントを書く際にも使用できます。
しかしながら、runnableExamplesで生成されるドキュメントからは
空白行や余分な空白、コメントが削除されてしまうので、
みやすさを確保したドキュメントを整備したいときには使用できません。
また、コメントを一緒に描画したい場合は##
でコメントを書かないと消されてしまいます。
## 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
で以下のドキュメントが生成されます。
runnableExamplesで書いたサンプルコードのほうは、改行が詰められてしまい、
コメントも消えていることがわかります。
runnableExamplesで、コンパイルして実行して正常終了しないコードが存在した場合は
nim doc
のときに異常終了します。
まとめ
自分でライブラリを書いたり、Nimの標準ライブラリのドキュメント整備に関わったりした際に
色々ハマって解決したときの情報を整理しました。
自分がハマった情報を全部ここに書き出したので、セクションごとの繋がりがないかもしれないです。
Nimユーザの助けになれば幸いです。