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を実行することです。



assert系でテストする


assertとdoAssert

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

そのうちよく使うのはassertdoAssertです。

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

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

追記:

assertはコンパイル時に無視できるので開発時用にプロシージャ内に埋め込んでおく、というアプローチが良いでしょう。

本番リリース時は--assertions:offオプションをコンパイルすることで本番リリースには含めない、という感じで。

runnableExamplesではdoAssertのみを使うようにしましょう。assertをdoAssertに統一するということを公式が一度行っています

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

doAssertはオプションを与えてもassertでのチェックを継続します。


a.nim

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を使うべきです。


unittestでテストする

多くの場合はこちらでテストコードを書きます。


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

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で変数を宣言すると便利です。

setuptestが実行される直前に都度実行されます。

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

% 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)


% 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ブロックも読み込まれてしまい、

テスト時に実行されてしまうためです。


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


% 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


参照型のテスト

Nimでは嬉しいことに配列やシーケンス、構造体の値比較が==で行えます。

参照型の値比較は==では行えないのですが、これも容易に回避する方法があります。

[]というプロシージャを使用することで参照型の値の取得が可能です。

これを利用して参照型の値比較も容易に行えます。

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

一部を抜粋して和訳。


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


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で以下のドキュメントが生成されます。

2019-06-13-171728_1920x1080_scrot.png

runnableExamplesで書いたサンプルコードのほうは、改行が詰められてしまい、

コメントも消えていることがわかります。

runnableExamplesで、コンパイルして実行して正常終了しないコードが存在した場合は

nim docのときに異常終了します。


まとめ

自分でライブラリを書いたり、Nimの標準ライブラリのドキュメント整備に関わったりした際に

色々ハマって解決したときの情報を整理しました。

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

Nimユーザの助けになれば幸いです。