17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-13

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系のプロシージャやテンプレートがいくつかあります。
そのうちよく使うのはassertdoAssertです。

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

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で変数を宣言すると便利です。
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を持っていない)

.gitignore
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

モジュールを分けずに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"

テストの実行

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

2019-06-13-171728_1920x1080_scrot.png

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

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

まとめ

自分でライブラリを書いたり、Nimの標準ライブラリのドキュメント整備に関わったりした際に
色々ハマって解決したときの情報を整理しました。

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

17
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?