現代のプログラミングにおいて、テストフレームワークは欠かせないものです。Zenは言語仕様の中にテストフレームワークを備えており、簡単にプログラムのテストが書けるようになっています。
本書内のほとんどのサンプルコードはZenのテストフレームワークを使用しているので、ここまでで何回もテストコードを目にしています。ここでは、改めて、Zenのテストフレームワークについて解説します。
テストを記述するためのブロックをテストブロックと呼びます。テストブロックは、test
キーワードから始め、"テストケース名" { テスト本体 }
と続きます。
テストブロックは、テストビルドの時のみビルドされます。通常のビルド時には、テストブロックはビルド対象になりません。そのため、プロダクトのバイナリサイズへの影響を気にすることなく、テストを記述することができます。
テストを実行するには、zen test ソースファイル名
コマンドを入力します。unittest.zen
ファイルを新規作成し、次のコードを入力して下さい。
examples/ch05-testing/basic/src/unittest.zen:1:6
const std = @import("std");
const ok = std.testing.ok;
test "simple test" {
ok(1 == 1);
}
このテストは意味のないテストではありますが、テストの概念を説明するのには十分です。テスト本体では、1 == 1
がtrue
になるかどうか、ok
関数でテストしています。もちろん結果はtrue
になるため、テストはパスします。
次のコマンドでテストを実行してみましょう。
zen test unittest.zen
次のような結果が得られたはずです。
1/1 test "simple test"...OK
All tests passed.
これはsimple test
というテストケースが無事パスして、unittest.zen
にある全てのテストがパスしたことを意味します。
zen test
は指定したソースファイルとそのソースファイルが依存するソースファイルからテストブロックを抽出し、ビルドした上でテストを実行します。
テストが失敗するとどうなるのでしょうか?unittest.zen
に次のコードを追加してみましょう。
test "test fail" {
ok(1 == 0);
}
このテストは1 == 0
がfalse
になるため、失敗します。
zen test unittest.zen
テストを実行するとコンソールに次のような結果が得られます。
1/11 test "simple test"...OK
2/11 test "test fail"...test failure
/lib/zen/std/testing.zen:30:9: 0x20893b in std.testing.ok (test)
@panic("test failure");
^
src/unittest.zen:9:7: 0x208b2e in test "test fail" (test)
ok(1 == 0);
^
...
Tests failed. Use the following command to reproduce the failure:
simple test
はパスしていますが、test fail
のテストケースは失敗しています。失敗した時のバックトレースが出力されており、ok(1 == 0)
が@panic
を呼び出したことがわかります。
テストブロックは通常のプログラムと同じファイル内に記述することができます。同じファイル内にテストブロックを記述する場合、プライベートな関数や変数に対するテストを書くことが可能です。
examples/ch05-testing/basic/src/unittest.zen:55:70
var private_global: u32 = 3;
fn incPrivateGlobal() void {
private_global += 1;
}
pub fn getPrivateGlobal() u32 {
return private_global;
}
test "can test private resources" {
// パブリックな関数はもちろんテスト可能
ok(getPrivateGlobal() == 3);
// プライベートな変数、関数もテスト可能
incPrivateGlobal();
ok(private_global == 4);
}
作成したソースファイルをユーザー目線からテストしたい場合、テストブロックを外部のソースファイルに書くと良いでしょう。
ok
関数は標準ライブラリ内に用意されているテスト用関数です。std.testing
モジュールにはいくつかテスト用関数が用意されています。
テストブロックでは前節で紹介したassert
ではなく、std.testing
のテスト用関数を用いるべきです。assert
はビルドモードがReleaseFast
モードとReleaseSmall
モードのときに最適化で取り除かれます。それに対して、std.testing
のテスト用関数は、ビルドモードに関わらず機能します。
先程作ったtest fail
のテストケースが含まれているunittest.zen
を--release-fast
オプションを付けてテストを実行してみます。
$ zen test src/unittest.zen --release-fast
1/2 test "simple test"...OK
2/2 test "test fail"...test failure
Tests failed. Use the following command to reproduce the failure
ok
関数が正しく機能し、test fail
テストケースが失敗していることがわかります。ただし、デバッグモードではないため、バックトレースは出力されません。
std.testing
内のテスト用関数を紹介します。
fn ok(b: bool) void
引数にブール値を1つとります。引数のブール値がtrue
であれば成功、false
であれば失敗となります。
fn equalSlices(comptime T: type, expected: []T, actual: []T) void
2つのスライスの一致比較を行います。3つの引数をとり、第一引数は配列要素の型、第二引数に期待値 (expected
)、第三引数に実際の値 (actual
) を与えます。expected
とactual
が一致していれば成功、一致していなければ失敗となります。
examples/ch05-testing/basic/src/unittest.zen:8:12
const equalSlices = std.testing.equalSlices;
test "equalSlices" {
const a = [_]u32{ 1, 2, 3, 4 };
equalSlices(u32, &a, &[_]u32{ 1, 2, 3, 4 });
}
次のように一致しない値を指定した場合はテストが失敗となります。
test "equalSlices - NG" {
const a = [_]u32{ 1, 2, 3, 5 };
equalSlices(u32, &a, &[_]u32{ 1, 2, 3, 4 });
}
テストを実行すると次のような結果が表示されます。expected input 5, found 4 (index 3 incorrect)
と表示されており、3番目の要素が異なることがわかります。
11/12 test "equalSlices - NG"...expected input 5, found 4 (index 3 incorrect)
lib/zen/std/testing.zen:302:9: 0x20c0e6 in std.testing.helper (test)
@panic(stream.written());
^
lib/zen/std/testing.zen:245:16: 0x20a858 in std.testing._equalSlices (test)
testing.helper(is_ok, prefix ++ "expected input {}, found {} (index {} incorrect)", .{ a, b, i });
^
lib/zen/std/testing.zen:231:5: 0x209baa in std.testing.equalSlices (test)
_equalSlices(T, expected, actual, "");
^
unittest.zen:128:5: 0x209850 in test "equalSlices - NG" (test)
equalSlices(u32, &a, &[_]u32{ 1, 2, 3, 4 });
^
...
Tests failed. Use the following command to reproduce the failure:
fn equalStrings(expected: []u8, actual: []u8) void
2つの文字列の一致比較を行います。文字列の比較をする場合は、equalSlices
よりequalStrings
が適切です。
2つの引数をとり、第一引数に期待値 (expected
)、第二引数に実際の値 (actual
) を与えます。expected
とactual
が一致していれば成功、一致していなければ失敗となります。
examples/ch05-testing/basic/src/unittest.zen:88:92
const equalStrings = std.testing.equalStrings;
test "equalStrings" {
const a = "FooBar";
equalStrings("FooBar", a);
}
次のように一致しない文字列を指定した場合はテストは失敗となります。
test "equalStrings - NG" {
const a = "FooFoo";
equalStrings("FooBar", a);
}
テストを実行すると次のような結果が表示されます。このように期待値と実際の値が上下に並べて表示されます。
11/12 test "equalStrings - NG"...
===== Expected =====
FooBar
====== Actual ======
FooFoo
====================
lib/zen/std/testing.zen:302:9: 0x20afec in std.testing.helper (test)
@panic(stream.written());
^
lib/zen/std/testing.zen:272:5: 0x209394 in std.testing.equalStrings (test)
helper(mem.equal(u8, expected, actual),
^
unittest.zen:133:5: 0x209840 in test "equalStrings - NG" (test)
equalStrings("FooBar", a);
^
...
Tests failed. Use the following command to reproduce the failure:
fn equal(expected: anytype, actual: @TypeOf(expected)) void
2つの引数を取り、その一致比較を行います。2つの引数が一致していれば成功、一致していなければ失敗となります。
第一引数に期待値、第二引数に実際の値を与えます。equal
はジェネリックな関数になっており、様々な型に対して使うことが可能です。2つの引数は型が同一である必要があります。また、引数の型はコンパイル時計算可能である必要があります。
以下に示すequal
は全て成功します。
examples/ch05-testing/basic/src/unittest.zen:14:39
const equal = std.testing.equal;
test "equal" {
// ブール型の比較
equal(true, true);
// comptime_int型の比較
equal(1, 1);
// u32型の比較
equal(@to(u32, 1), @to(u32, 1));
// 配列型 ([5]u8) の比較
equal("hello", "hello");
// スライス型 ([]u8) の比較
equal("hello"[0..], "hello"[0..]);
// ポインタ型の比較
var x: u32 = 0;
equal(&x, &x);
// 構造体型の比較
const Point = struct {
x: u32 = 0,
y: u32 = 0,
};
var point1 = Point{};
var point2 = Point{};
equal(point1, point2);
}
次のように一致しない値を指定した場合はテストは失敗となります。
test "equal - NG" {
// 構造体型の比較
const Point = struct {
x: u32 = 0,
y: u32 = 0,
};
var point1 = Point{ .x = 0, .y = 0 };
var point2 = Point{ .x = 0, .y = 1 };
equal(point1, point2);
}
テストを実行すると次のような結果が表示されます。field: y expected input u32{0}, found 1
と表示されており、フィールド y
の値が異なることがわかります。
11/12 test "equal - NG"...field: y expected input u32{0}, found 1
lib/zen/std/testing.zen:302:9: 0x210feb in std.testing.helper (test)
@panic(stream.written());
^
lib/zen/std/testing.zen:115:19: 0x20d66e in std.testing._equal (test)
=> testing.helper(
^
lib/zen/std/testing.zen:159:21: 0x20b3f0 in std.testing._equal (test)
_equal(@field(expected, field.name), @field(actual, field.name), message_prefix ++ stprefix ++ field.name, true);
^
lib/zen/std/testing.zen:56:5: 0x20a1da in std.testing.equal (test)
_equal(expected, actual, "", true);
^
unittest.zen:144:5: 0x20997c in test "equal - NG" (test)
equal(point1, point2);
^
...
Tests failed. Use the following command to reproduce the failure:
fn notEqual(unexpected: anytype, actual: @TypeOf(unexpected)) void
2つの引数を取り、その一致比較を行います。2つの引数が一致していなければ成功、一致していれば失敗となります。
第一引数に期待値、第二引数に実際の値を与えます。notEqual
はジェネリックな関数になっており、様々な型に対して使うことが可能です。2つの引数は型が同一である必要があります。また、引数の型はコンパイル時計算可能である必要があります。
以下に示す notEqual
は全て成功します。
examples/ch05-testing/basic/src/unittest.zen:98:124
const notEqual = std.testing.notEqual;
test "notEqual" {
// ブール型の比較
notEqual(true, false);
// comptime_int型の比較
notEqual(1, 2);
// u32型の比較
notEqual(@to(u32, 1), @to(u32, 2));
// 配列型 ([5]u8) の比較
notEqual("hello", "world");
// スライス型 ([]u8) の比較
notEqual("hello"[0..], "world"[0..]);
// ポインタ型の比較
var x: u32 = 0;
var y: u32 = 0;
notEqual(&x, &y);
// 構造体型の比較
const Point = struct {
x: u32 = 0,
y: u32 = 0,
};
var point1 = Point{ .x = 0, .y = 0 };
var point2 = Point{ .x = 1, .y = 2 };
notEqual(point1, point2);
}
次のように一致する値を指定した場合はテストが失敗となります。
test "notEqual - NG" {
// 構造体型の比較
const Point = struct {
x: u32 = 0,
y: u32 = 0,
};
var point1 = Point{ .x = 0, .y = 0 };
var point2 = Point{ .x = 0, .y = 0 };
notEqual(point1, point2);
}
テストを実行すると次のような結果が表示されます。same structure Point{ .x = 0, .y = 0 }
と表示されており、Point 構造体の値が一致していることがわかります。
11/12 test "notEqual - NG"...same structure Point{ .x = 0, .y = 0 }
lib/zen/std/testing.zen:302:9: 0x20d7cb in std.testing.helper (test)
@panic(stream.written());
^
lib/zen/std/testing.zen:167:24: 0x20b43b in std.testing._equal (test)
testing.helper(!cmp, message_prefix ++ sep ++ "same structure {}", .{expected});
^
lib/zen/std/testing.zen:60:5: 0x20a15a in std.testing.notEqual (test)
_equal(unexpected, actual, "", false);
^
unittest.zen:171:5: 0x2098fc in test "notEqual - NG" (test)
notEqual(point1, point2);
^
...
Tests failed. Use the following command to reproduce the failure:
fn err(expected_error: anyerror, actual_error_union: anytype) void
期待値にエラー型のエラー種別を、実際の値にエラー共用体を、それぞれ引数に取り、エラー共用体の値がエラー型でかつ期待値通りのエラー種別であるかどうか、検証します。
examples/ch05-testing/basic/src/unittest.zen:41:48
const err = std.testing.err;
const Error = error {
AnError,
};
test "err" {
const e: Error!u32 = Error.AnError;
err(Error.AnError, e);
}
次のように期待値と異なるエラーを指定した場合はテストが失敗となります。
const Error2 = error{
AnError,
OtherError,
};
test "err - NG" {
const e: Error2!u32 = Error2.OtherError;
err(Error2.AnError, e);
}
テストを実行すると次のような結果が表示されます。expected error.AnError, found error.OtherError
と表示されており、期待値と異なるエラーであることがわかります。
11/12 test "err - NG"...expected error.AnError, found error.OtherError
lib/zen/std/testing.zen:302:9: 0x20b5ec in std.testing.helper (test)
@panic(stream.written());
^
lib/zen/std/testing.zen:47:20: 0x20a223 in std.testing.err (test)
testing.helper(false, "expected error.{}, found error.{}", .{ @errorName(expected_error), @errorName(actual_error) });
^
unittest.zen:164:5: 0x2098db in test "err - NG" (test)
err(Error2.AnError, e);
^
...
Tests failed. Use the following command to reproduce the failure:
環境依存なテストなどの特定条件でのみ実行するテストを、スキップしたい場合があります。
テストケースをスキップするには、テストケースからerror.SkipZenTest
を返します。
examples/ch05-testing/basic/src/unittest.zen:94:96
test "Skip Test" {
return error.SkipZenTest;
}
Zenでは、依存するソースファイルに含まれるテストも自動的に抽出します。新しくanother_unittest.zen
というファイルを次の内容で作成して下さい。
examples/ch05-testing/basic/src/another_unittest.zen:1:10
const std = @import("std");
const ok = std.testing.ok;
pub fn funcInAnotherFile() bool {
return true;
}
test "test in another file" {
ok(funcInAnotherFile());
}
unittest.zen
でanother_unittest.zen
をインポートし、funcInAnotherFile
関数を呼び出すコードを追加します。Zenはインポートしただけでは依存関係があるとみなさないことに注意して下さい。インポートした後、何らかの関数や定数を実際に利用して初めて、依存関係があるとみなされます。
examples/ch05-testing/basic/src/unittest.zen:50:53
const another_unittest = @import("another_unittest.zen");
test "call function in another file" {
ok(another_unittest.funcInAnotherFile());
}
この状態でunittest.zen
のテストを実行します。
$ zen test unittest.zen
1/6 test "simple test"...OK
2/6 test "equalSlices"...OK
3/6 test "equal"...OK
4/6 test "err"...OK
5/6 test "call function in another file"...OK
6/6 another_unittest.test "test in another file"...OK
All tests passed.
一番下で、another_unittest.zen
にあるtest in another file
がテストされていることがわかります。このように、Zenのテストでは依存関係のあるファイルを自動的にテストします。
zen test
では1つのソースファイルを指定します。しかし、依存関係にない複数のファイルをまとめてテストしたい場合には少し不便です。もちろん便利にする方法があります。
新しくtest.zen
というファイルを、次の内容で作成して下さい。
examples/ch05-testing/basic/src/test.zen:1:4
comptime {
_ = @import("unittest.zen");
_ = @import("another_unittest.zen");
_ = @import("user_test.zen");
}
このようにcomptime
ブロック内でテストしたい対象ファイルをインポートすることで、ファイル同士の依存関係を作ることができます。
テストを実行すると、次のようになります。
$ zen test src/test.zen
1/6 unittest.test "simple test"...OK
2/6 unittest.test "equalSlices"...OK
3/6 unittest.test "equal"...OK
4/6 unittest.test "err"...OK
5/6 unittest.test "call function in another file"...OK
6/6 another_unittest.test "test in another file"...OK
All tests passed.
このようにテストしたいファイルへの依存関係をtest.zen
に記述しておき、テストを実行することが慣習になっています。
--test-filter
オプションにより、実行するテストを指定することができます。
上で作ったtest.zen
のうち、simple test
だけ実行してみます。
$ zen test src/test.zen --test-filter "simple test"
1/1 unittest.test "simple test"...OK
All tests passed.
フィルターは部分一致です。フィルターの文字列を含む全てのテストケースが実行されます。
$ zen test src/test.zen --test-filter "equal"
1/2 unittest.test "equalSlices"...OK
2/2 unittest.test "equal"...OK
All tests passed.
☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.