まえがき
- testament というツールの使い方を説明します
- testament 用のテストコードを紹介します
testament とは
Nim公式のテストランナーです。Nim自体の開発にも使われている実績があります。
testamentはNimをインストールした時に一緒にインストールされます。
testament に関するドキュメントは testament に記載されています。
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が提供されています。
こちらのモジュールを使って単体テストを実施するのではダメなのでしょうか?
unittestの使い方については Nim 0.20 の頃に以下の記事にまとめました。
Nim 0.20は事実上Nim 1.0のRC版らしいのでNimのテストコードを書くためのTIPS
こちらを使ってもダメなわけではないです。
しかしながら、上記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
前述の説明からは、将来的なunittestモジュールの削除の可能性については分かりません。
実際unittestモジュールを使用しているサードパーティのライブラリは多数存在します。
いきなりunittestモジュールをdeprecatedにして削除することは考えにくいです。
ですが、公式としてはtestamentを使うことを推奨しているように受け取れます。
これを踏まえて、「testamentを使うテストの書き方」「テス
トの実施方法」「unittestモジュールは使ってはいけないのか」を調査しようと思い至りました。
testamentを使ってみる
兎にも角にもtestamentを使ってみます。
testamentに書かれている内容と一部重複します。
シンプルな単体テスト
まず、一番単純な単体テストのサンプルを示します。
testamentではデフォルトでtests
ディレクトリ配下のファイル拡張子nimのファイルをテスト対象のコードと判定します。
以下のNimプログラムを作成します。
doAssert 1 == 1
次に、testament run <ファイル名|モジュール名>
を実行すると、テストが実行されます。
(フルパスでも相対パスでもなく、ファイル名)
$ 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>
を使用します。
$ 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
というファイルが生成されます。
$ testament html
Used /home/jiro4989/.nimble/bin/nim to run the tests. Use --nim to override.
$ ls testresults.html
testresults.html
このファイルをブラウザで開いてみると、以下の画面が表示されます。
見やすく整えられたレポートが確認できました。
testament用のテストコードを書く
testamentの基本的な使い方がわかったところで、testament用のテストコードを書いて
いきます。testament用のテストコードの書き方についてはtestamentにもサンプルが載っているので重複する内容があります。
specを書く
テストファイルごとの、テストする際の設定やテスト期待値、入力などを制御する設定を書きます。
これらの設定をtestamentでは spec と呼びます。
specは省略可能です。
specはファイルの先頭に以下のような文字列を定義することで表現します。
discard """
exitcode: 0
"""
# 以降はテストコード
doAssert 1 == 1
このdiscardしている文字列に定義可能な設定の詳細はtestamentの
「Writing Unittests」を参照してください。
定義可能な設定はたくさん存在しますが、頻繁に使用すると思ったものをピックアップして説明します。
action
テスト実行時の動作を設定します。
設定可能な値は以下の3つです。
-
compile
コンパイルが成功することを期待します -
run
コンパイルとプログラムの実行が成功することを期待します -
reject
コンパイルが失敗することを期待します
discard """
action: "run"
"""
exitcode
テストが終了した時の終了コードを設定します。
exitcodeで、テストの成功を期待したい場合は 0 を設定します。
テストの失敗を期待したい場合は 1 を設定します。
デフォルトでは 0 が設定されています。
discard """
exitcode: 0
"""
output
テストが終了した時の期待する標準出力を設定します。
outputを設定した場合は、そのテストコードが標準出力に書き出す文字列がoutputと一
致しない場合にテスト失敗と判定されます。
output未設定の場合は、そのテストコードが何を標準出力しても無視されます。
discard """
output: '''
hello world
'''
"""
echo "hello world"
input
テストを実行する際に、標準入力に文字列を渡します。
discard """
input: '''
hello world
'''
"""
doAssert stdin.readline == "hello world"
errormsg
コンパイルエラーになったときのエラーメッセージを設定します。
以下はfunc
を使用してプロシージャを定義しようとするものの、echo
が副作用を持
っているためコンパイルエラーになる例です。
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にする例です。
discard """
cmd: "nim c --gc:orc -d:debugFlag -r $file"
"""
const debugFlag {.booldefine.} = false
doAssert debugFlag == true
target
テストの実行ターゲットを指定します。
Nimでは C, C++, JavaScript などがコンパイル時に指定できますが、ここでもその指定が可能です。
複数指定する場合は、半角スペースで区切って入力します。
discard """
target: "c cpp js"
"""
doAssert true
上記設定をした上でtestamentを実行すると、以下のように3つのテストが走っていることが確認できます。
$ 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
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つを使ってはいけない理由は後述します。
doAssert 1 + 1 == 2
# または
import unittest
check 1 + 1 == 2
僕はunittest.check
を使うのをオススメします。理由は2つあります。
- テストがエラーになっても後続のテストが実行される
- テスト失敗理由がわかりやすい
unittest.check
以下にunittest.check
を使うサンプルを用意しました。
discard """
output: '''
1
'''
"""
import unittest
echo 1
check 1 == 1
check 1 == 1
check 1 == 1
このテストコードに対してtestamentを実行してみると、正常にパスします。
$ testament r t_unittest
PASS: tests/t_unittest C ( 0.63 sec)
次に、このコードをわざと失敗するように修正してみます。
discard """
output: '''
1
'''
"""
import unittest
echo 1
check 3 == 1
check 2 == 1
check 1 == 1
このテストコードに対してtestamentを実行してみると、エラーになります。
そして、テストが失敗した箇所がすべてメッセージとして出力されます。
⟩ 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
を使った場合は以下のようになります。
doAssert false, "error 1"
doAssert false, "error 2"
doAssert false, "error 3"
上記コードをtestamentで実行してみます。
$ 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がこのように例外テストを行っています。
以下のケースではtestamentはPASSします。
discard """
output: "1"
"""
import unittest
expect IndexDefect:
var n = @[0, 1, 2]
discard n[255]
echo 1
以下のように、期待値と異なる例外が返って来た場合はtestamentはエラーになります。
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のコードを書く時は、このディレクトリ配下のコードを大いに参考にしました。
サードパーティのパッケージでtestamentを使用しているものはあまり見たことないのですが、
WebフレームワークのPrologueがtestamentを使用してテストを行っていました。
testamentでunittestモジュールは使えないの?
結局のところtestamentはNimのファイルを1つ1つコンパイルして実行して、
specに書かれた設定をもとに標準出力や終了コードを比較してるだけの原始的なテストランナーのようでした。
それだったらunittestモジュールもtestament内で使用できるのでは?と思いました。
前述のunittest
モジュールの説明では「unittest.nimの代わりに」という記載がありました。
公式ドキュメントのBest practiciesではunitest.checkを使うように書いてありました。
実際のところtestamentでunittestモジュールを使ってはいけないのでしょうか?
これは「使って良い」です。
ただし**「一部」使ってはいけない機能が存在します**。
Writing tests stdlibによると「unittest.suite
とunittest.test
は使ってはいけない」ようです。
Don't use
unittest.suite
andunittest.test
.
その理由は、おそらくですが unittest.suite
と unittest.test
が標準出力にメッセージを出力する
ことが原因ではないかと考えています。
というのも、unittest.suiteを使用してもoutputを指定しなければ問題はありませんでした。
以下のtestament用のテストコードは正常に動作します。
import unittest
suite "suite":
test "test":
check true
しかしながら、specにoutputを設定した時に問題が発生します。
前述のコードを以下に修正しました。
specに output 1 を追加しただけです。
discard """
output: '''
1
'''
"""
import unittest
suite "suite":
test "test":
echo 1
check true
この状態でtestamentを実行してみます。
結果は以下のとおりです。
⟩ 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でのテスト実施の一助となれば幸いです。