D言語にはdubというパッケージマネージャ兼ビルドツール兼テストツールがあります。
今回はdubのテストツールとしての側面について調べがついたことを述べます。
概要
- dub testで思い通りにいかない場合は、以下をチェック
- 設定にconfigurationを記載すること
- そのconfigurationのtargetTypeを指定すること
- dub test時にデフォルトで選択される"unittest" configuratinも明示的に記載すること
- targetTypeがexecutableなら、mainSourceFileも指定すること
dub testつかおう
dub test
とコマンドを打つと、dubプロジェクトをテストすることができます。
一言で書くとそうなのですが、実際に使おうと思うといくつか障壁があります。
- メイン関数実行されない問題
- メイン関数ダブる問題
- メイン関数がないって言われる問題
- --main-file指定すると挙動違う問題
- ライブラリとアプリケーションで挙動違う問題
- mainSorceFile指定すると挙動違う問題
- sourcePaths指定すると挙動違う問題
- configurationsがあると挙動違う問題
dub test
は、メイン関数を含む単体テスト用メインソースファイルを自動生成して一緒にコンパイルしてくれるという機能を持っています。
クッソおせっかいな機能で、これが随所で邪魔をしてきます。
これがあるので私は最近までdub test
を使わずにいました。
でもせっかくある機能なのだから有効活用したい…ので、この意味不明な挙動を完全に掌握し、先述の各種問題をコントロール下に置きたい!と思ったので調べました。
ルールを覚えてdub testがやってくれることを理解しよう
dub testのおせっかいにはルールがあります。ルールを覚えると何をすればいいのかがおのずと見えてくるはず…!
ルール1: メインソースファイル
メインソースファイルというのは、メイン関数だけを含むのソースファイルのことです。
dub testは単体テスト用メインソースファイルを自動的に生成し、メインソースファイルと置き換えます。
通常ライブラリではメインソースファイルを含めませんが、単体テストではメイン関数を含む実行ファイルにしなければならないため、dub testでは単体テスト用メインソースファイルを自動的に生成して、一緒にコンパイルしてくれます。
一方、通常のアプリケーションでは、単体テストでメイン関数が実行されてしまうと困る場合が多々あります。サーバーアプリやGUIアプリなんかを考えるとわかりやすいですね。dub test
で一発で結果が出ずに、サーバーアプリではクライアントとのやり取り、GUIではユーザーの操作なんかが必要となり、再現性のあるテストができなくなるからです。(これらはdub test
のスコープ外です)
このため、dub testでは実際のメイン関数が実行されないようにメインソースファイルと、空のメイン関数を含む単体テスト用メインソースファイルを置き換えます。
でもまあ、ライブラリであっても設定ファイル(dub.json/dub.sdl)でmainSourceFile
を指定していれば、単体テスト用メインソースファイルと置き換わります。
このmainSourceFile
置き換えという仕様が、障壁1.「メイン関数実行されない問題」の原因です。また、逆にmainSourceFile
が指定されていないと、単純にすべてのソースファイルに加えて、単体テスト用メインソースファイルが追加されてビルドされ、自分で定義したメイン関数とダブってしまい、障壁2.「メイン関数ダブる問題」を引き起こしてしまいます(条件コンパイルで回避する必要があります)。
さらに注意事項として、source/app.d
などのファイルがあっても、それが必ずmainSourceFile
として推測されるわけではありません。source/app.d
があった場合にそれがアプリケーションのメインソースファイルと推測されるのは、configurationがない場合に限るのです。(つまり、一つでもconfigurationがある場合、ちゃんとmainSourceFile
を指定しないとメイン関数が重複してしまいます。)
ルール2: --main-file
オプション
ルール1で、dub testでは単体テスト用メインソースファイルが使われると言いました。この--main-fileオプションは、dubが生成した単体テスト用メインソースファイルの代わりにメイン関数を定義したファイルを追加で指定ができるというオプションです。ただし、ライブラリの場合限定です。(なぜライブラリだけで使えるか、の理由は不明です)
メイン関数を含むファイルを自作したい場合はこのオプションを指定してやることで、勝手に作ったファイルではなく、自分自身が作ったファイルでテストを行うことができます。
なお、このファイルには必ずメイン関数が必要です。これを忘れると障壁3.「メイン関数がないって言われる問題」の原因になります。
なんか、これを指定するとルール3が適用されなかったりとかなんやかんやあるみたいで、障壁4.「--main-file指定すると挙動違う問題」につながっていきます。
ルール3: unittest configuration
dub testでは、--config
で構成を指定しない場合で、dub設定の中にunittest
という名前の構成(configuration)がある場合、それを利用するというおせっかいもあります。使用される構成は、以下のようにして決定されます。
-
--config
で指定される構成 -
unittest
という名称の構成 - 最初に出てくるライブラリの構成
- 最初に出てくるアプリケーションの構成
要するに dub build と違って、最も最初に出てくる構成が選ばれるわけではない、というのがポイントです。
どうすればいいのか?
上に簡単に書きましたが、実際にはもっと複雑です。例えばconfigurationがない場合や、source/app.dなどの有無、sourcePathsの設定など。主に、デフォルト構成の自動生成周りでいろいろと予想外?な動きをするようです。(この辺が障壁5~8の原因)
この辺りは複雑すぎてまじめに説明するとdubのプログラム内部の詳細を書くような感じになってしまうので(dubは残念ながらそういう感じの残念コードです)、以下に失敗しないためのテンプレを記載しておきます。
アプリケーションの場合その1
以下に従いプロジェクトを作成します。
- "application" configurationを作成する(名称は任意だが、デフォルトで選択されるように、一番先頭に定義する。ちなみにアプリケーションタイプの場合、configurationの定義を省略するとデフォルトは"application"になる。)
- "unittest" configurationを必ず作成する
- mainSourceFileを必ず指定する
- mainSourceFileには、メイン関数だけを記載すること。(単体テストや、ほかの関数は一切含めない。)
- dub test実行時には、理由がない限り
--config
は使用しない。- どうしても指定したい場合は、指定するconfigにもmainSourceFileを指定する。
- dub test実行時には、理由がない限り
--main-file
は使用しない。
{
"authors": ["作者名"],
"description": "プロジェクトの説明",
"license": "public domain",
"name": "プロジェクト名",
"configurations": [
{
"name": "application",
"targetType": "executable",
"mainSourceFile": "source/app.d"
},
{
"name": "unittest",
"mainSourceFile": "source/app.d"
}
]
}
import myapp.main;
// 自分で作ったメイン関数(myMain)呼ぶだけみたいなのが望ましい。
int main(string[] args) { return myMain(args); }
アプリケーションの場合その2
以下に従いプロジェクトを作成します。メイン関数書くところにもっといろいろ書きたいとか、dub testのためだけに関数コールの階層深くなるの嫌なんだけどっていう人向け。※でも結局version (unittest)
みたいなのを書かないと行けなくてちょっと汚くなる
- "application" configurationを作成する(名称は任意だが、デフォルトで選択されるように、一番先頭に定義する。ちなみにアプリケーションタイプの場合、configurationの定義を省略するとデフォルトは"application"になる。)
- "unittest" configurationを必ず作成する
- mainSourceFileは指定しない("unittest" configurationの中身だけでなく、外側にも指定しないこと)
- dub test実行時には、理由がない限り
--main-file
は使用しない。 - 念のため、source/app.d や src/main.d などの名称は避ける(今のところmainSourceFileだと推定はされないはずだが、将来的にはわからない)
- メイン関数は、
version (unittest)
などで、dub test時には定義されないようにすること。
{
"authors": ["作者名"],
"description": "プロジェクトの説明",
"license": "public domain",
"name": "プロジェクト名",
"configurations": [
{
"name": "application",
"targetType": "executable"
},
{
"name": "unittest",
"versions": ["DubTest"]
}
]
}
import hoge.foo;
pragma(lib, "fuga");
// 条件コンパイルでdub testのときはメイン関数が定義されないようにすること
// ここでは unittest 全部で定義されないようにしていますが、
// 上記設定ファイルのように"DubTest"というバージョンが定義されているのであれば、
// 代わりにDubTestとしても大丈夫です。
// これは `dub build -b=unittestでビルドだけ行って、その後実行したい場合に便利です。
version (unittest) { } else
int main(string[] args) { return foo(args); }
ライブラリの場合
以下に従いプロジェクトを作成します。ただし、configurationによってライブラリにもアプリケーションにもなりうるプロジェクトの場合は、アプリケーションの場合を優先して実施してください。
- "library" configurationを作成する(名称は任意だが、デフォルトで選択されるように、一番先頭に定義する。ちなみにライブラリタイプの場合、configurationの定義を省略するとデフォルトは"library"になる。)
- "unittest" configurationを必ず作成する
- mainSourceFileは指定しない("unittest" configurationの中身だけでなく、外側にも指定しないこと)
- dub test実行時には、理由がない限り
--config
は使用しない。 - dub test実行時には、理由がない限り
--main-file
は使用しない。
{
"authors": ["作者名"],
"description": "プロジェクトの説明",
"license": "public domain",
"name": "プロジェクト名",
"configurations": [
{
"name": "library",
"targetType": "library"
},
{
"name": "unittest",
"targetType": "library"
}
]
}
#まとめ
いろいろ書きましたが、要するに設定ファイルでconfigurationにunittest構成を作り、targetTypeをちゃんと書いて、それがexecutableな場合はmainSourceFileを指定する(またはversionでdub test時にメイン関数が定義されないようにする)ことで、dub testで比較的素直に動かすことができる、ということでした。
今回この記事を書くにあたり、ソースコードの中身まで読んで調べていますが、勘違いやもっといい方法を見落としているかもしれません。もしご指摘ありましたら、コメントしていただけると助かります。