6
2

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 1 year has passed since last update.

D言語Advent Calendar 2021

Day 22

D言語のBetterCモードで単体テストする方法

Last updated at Posted at 2021-12-21

今年のD言語フォーラムでは特にメモリ管理が多く話題に上がりました
メモリ管理の話題の多くはD言語のGCに対する批判や、参照カウント方式の採用など、GCを使わずにD言語を使いたいというものでした。
「GCなしでもD言語は使えるのだから、GCが嫌って理由でD言語使わないとか言うのは甘え」みたいに言う人もいますが、実際のところそんなに簡単にGCを切り離すことはできません。
GCを簡単に切ることができない主要な要因は標準ライブラリ(Phobos)が使用できなくなることが大きいです。代替となるGCなしで動作する強力なライブラリがあれば、D言語はGCが…みたいな意見も少なくなるかもしれません。

そこで今回は、BetterCで動作するライブラリを書くための基本の一つ、BetterCでもunittestを動作させる方法について紹介したいと思います。

概要

  • 普通に書くとbetterCでテストできない
  • 単体テストを実行するテスト関数を書く
  • unittestの定義に、betterCかそうじゃないかで実行有無を切り替えるよう手を加える
  • dub.jsonで専用のconfigurationを追加する
  • $ dub run -c=unittest-bc -b=unittest でBetterCのテストを実行

普通にunittestできないのか?

BetterCを使用すると、D言語のランタイムが利用できなくなるため、unittestを実行することができません。D言語のunittestはdruntimeでmain関数を呼び出す前に実行されているのです。
逆に、druntimeでやっているようなことを代替する方法があれば、BetterCでもunittestを動作させることができます。

BetterCでのテスト記載方法

ここでは、hogeというライブラリを作ることを想定し、それをテストすることを考えます。

以下のコードを、例えば hoge.ut などのモジュールに組み込みます。

source/hoge/ut.d
module hoge.ut;

version (D_BetterC)
{
	/// static ifで使えるBetterC判定
	enum bool isBetterC = true;
}
else
{
	/// static ifで使えるBetterC判定
	enum bool isBetterC = false;
}

/// モジュール一覧を列挙する
private static immutable allModules = [
	"hoge.foo",
	"hoge.bar",
	"hoge.buz"
];

/// インポート
private template from(string modname)
{
	mixin("import from = ", modname, ";");
}

/***************************************************************
 * 単体テスト実行関数
 *
 * unittestを実行するコードは、
 * `-betterC`かつ`-unittest`かつ`-version=HogeTest`
 * を指定した場合のみ実行
 */
version(D_BetterC) version(unittest) version(HogeTest)
extern (C) int main(int argc, const char** argv)
{
	import core.stdc.stdio;
	static foreach (mod; allModules)
	{
		printf("Testing ...%s\n", mod.ptr);
		static foreach (test; __traits(getUnitTests, from!mod))
		{
			printf("ut ...... %s\n", test.stringof.ptr);
			test();
		}
	}
	printf("Test completed!\n");
	return 0;
}

次に、 hoge.foo などでの単体テストの書き方です。

source/hoge/foo.d
module hoge.foo;

import hoge.ut: isBetterC;

// BetterCでもBetterCじゃなくても実行されるテスト
@nogc nothrow unittest
{
	// 単体テストを書く
	import core.stdc.stdio;
	printf("Both betterC or not.\n");
}
// BetterCの時にしか実行されないテスト
static if (isBetterC) @nogc nothrow unittest
{
	// 単体テストを書く
	import core.stdc.stdio;
	printf("Only betterC.\n");
}
// BetterCの時には実行されないテスト
// (通常のDランタイム・Phobosを使うテスト)
static if (!isBetterC) unittest
{
	// 単体テストを書く
	import std.stdio;
	writeln("Not betterC.");
}

そして、ビルドするためのdub.jsonは以下のようになります。

dub.json
{
	"authors": ["SHOO"],
	"copyright": "Copyright © 2021, SHOO",
	"description": "Hoge library",
	"license": "public domain",
	"name": "hoge",

	"configurations": [
		{
			"name": "library",
			"targetType": "library"
		},
		{
			"name": "unittest",
			"targetType": "library"
		},
		{
			"name": "unittest-bc",
			"targetName": "hoge-test-unittest-bc",
			"versions": ["HogeTest"],
			"buildOptions": ["betterC"],
			"targetType": "executable"
		}
	]
}

テストの実行

通常テストはこう

$ dub test
Generating test runner configuration 'hoge-test-unittest' for 'unittest' (library).
Performing "unittest" build using P:\app\dmd\bin64\dmd.exe for x86_64.
hoge ~master: building configuration "hoge-test-unittest"...
Linking...
Running hoge-test-unittest.exe
Both betterC or not.
Not betterC.
1 modules passed unittests

BetterCでテストするときはこう

$ dub run -c=unittest-bc -b=unittest
Performing "unittest" build using P:\app\dmd\bin64\dmd.exe for x86_64.
hoge ~master: building configuration "unittest-bc"...
Linking...
Running hoge-test-unittest-bc.exe
Testing ...hoge.foo
ut ...... __unittest_L6_C24()
Both betterC or not.
ut ...... __unittest_L13_C46()
Only betterC.
Testing ...hoge.bar
Testing ...hoge.buz
Test completed!

仕組み

isBetterC

unittestを書く際に、BetterCの時にテストしたいのか、そうじゃないとき(通常時)にテストしたいのかを使い分ける際、static if文で使用できるようにenum値を定義します。
enumで定義すると何がうれしいのかというと、 static if (!isBetterC) とかけるのがうれしいです。これを定義しないと、 version (D_BetterC) {} else と書かなければいけないので煩わしいです。

unittestの書き方

通常、BetterCであればPhobosがあってもなくても動くので、BetterCで動くことを確認すれば十分です。しかし、テストコードくらいはPhobosありで楽に書きたい…ということがあります。そのような場合は、static if (!isBetterC)を用います。

dub.jsonの中身

各コンフィグレーションを記載しているのがポイントです。

  • "name": "library" :
    今回の例ではライブラリとして構成しているので、通常はこのコンフィグレーションが使われます。-unittest-betterCは、このライブラリを使う側が制御します。
  • "name": "unittest" :
    dub test したときにデフォルトで使われるコンフィグレーションです。つまり、このコンフィグレーションの時には、 -unittestはつくけど-betterCはつきません。
  • "name": "unittest-bc" :
    BetterCでテストする場合に、 dub run -c=unittest-bc -b=unittest という感じに明示的に使うためのコンフィグレーションです。つまり、このコンフィグレーションの時には、 -unittest-betterCもつきます。この時にBetterCのテスト(テスト関数の呼び出し)を行うので、"versions": ["HogeTest"]でテストを行うための条件付けを行っています。

ちなみに、dub test -c=unittest-bc -b=unittestだとダメなのか?という疑問がわくかもしれませんが、dub testコマンドが勝手に生成する*_dub_test_root.dというテスト関数を含むモジュールがBetterCではないため、コンパイルに失敗してしまいます。dubのmainSourceFile--main-fileの指定などを駆使しても回避することはできません。

テスト関数

モジュール一覧の列挙が面倒くさいです。
以前一瞬だけ __traits(allMembers, moduleName) とすることで public import されたモジュールも検索することができたことがありましたが、今はできないので、残念ながら列挙が必要です。ここに新規作成したモジュールを追加するのを忘れがちなので注意が必要です。 core.reflection が導入されたら状況が変わるかな?

テスト関数は、今回はライブラリなので、単純にC言語のmain関数を定義します。
アプリケーションの場合は、main関数の中の先頭でテスト時にのみ単体テストする関数を呼び出すとよいでしょう。
特に version(D_BetterC) version(unittest) version(HogeTest) で定義されるパターンを絞っているのがポイントです。
今回はライブラリですので、基本的にメイン関数が定義されてしまうのはマズいです。
ですので、本当に必要な時に限って定義するよう調整しています。
まあ、実のところ dub run -c=unittest-bc -b=unittest の時にだけ定義できれば良いので、 version (HogeTest) だけが最低条件です。

テスト関数の中身では…
static foreach (mod; allModules) ですべてのモジュールを巡回して
from!mod でそのモジュールをimportし、
static foreach (test; __traits(getUnitTests, from!mod)) で各モジュールの単体テストを抜き出して各テストをtestという識別子にaliasしています。
test()とすることで、unittestを実行しています。
それ以外のprintfは、適当なお飾りです。

おわりに

今回はBetterCでの単体テスト方法について紹介しました。
実はこの方法、Phobosでも一部取り入れられているようです。(というか今回の記事のベースはPhobosの方法を参照しました)

以下のコードがそれです。ご参照ください。

6
2
0

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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?