オフィスアワーがそろそろ始まるよ!()

ユニットテスト

現代のプログラミングにおいて、テストフレームワークは欠かせないものです。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 == 1trueになるかどうか、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 == 0falseになるため、失敗します。

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);
}

作成したソースファイルをユーザー目線からテストしたい場合、テストブロックを外部のソースファイルに書くと良いでしょう。

std.testing

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内のテスト用関数を紹介します。

ok

fn ok(b: bool) void

引数にブール値を1つとります。引数のブール値がtrueであれば成功、falseであれば失敗となります。

equalSlices

fn equalSlices(comptime T: type, expected: []T, actual: []T) void

2つのスライスの一致比較を行います。3つの引数をとり、第一引数は配列要素の型、第二引数に期待値 (expected)、第三引数に実際の値 (actual) を与えます。expectedactualが一致していれば成功、一致していなければ失敗となります。

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:

equalStrings

fn equalStrings(expected: []u8, actual: []u8) void

2つの文字列の一致比較を行います。文字列の比較をする場合は、equalSlicesよりequalStringsが適切です。

2つの引数をとり、第一引数に期待値 (expected)、第二引数に実際の値 (actual) を与えます。expectedactualが一致していれば成功、一致していなければ失敗となります。

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:

equal

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:

notEqual

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:

err

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

Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8

Chapter 9

Chapter 10

Chapter 11

Chapter 12

Chapter 13

Chapter 14

Chapter 15

Appendix

Error Explanation

☰ 人の生きた証は永遠に残るよう ☰
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.