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

ユニットテスト

現代のプログラミングにおいて、テストフレームワークは欠かせないものです。Zenは言語仕様の中にテストフレームワークを備えており、簡単にプログラムのテストが書けるようになっています。

本書内のほとんどのサンプルコードはZenのテストフレームワークを使用しているので、ここまでで何回もテストコードを目にしています。ここでは、改めて、Zenのテストフレームワークについて解説します。

テストブロックの定義

テストを記述するためのブロックをテストブロックと呼びます。テストブロックは、testキーワードから始め、"テストケース名" { テスト本体 }と続きます。

テストブロックは、テストビルドの時のみビルドされます。通常のビルド時には、テストブロックはビルド対象になりません。そのため、プロダクトのバイナリサイズへの影響を気にすることなく、テストを記述することができます。

テストを実行するには、zen test ソースファイル名コマンドを入力します。unittest.zenファイルを新規作成し、次のコードを入力して下さい。

examples/ch05-testing/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/2 test "simple test"...OK
2/2 test "test fail"...test failure
zen/build/lib/zen/std/testing.zig:149:14: 0x20402b in std.testing.ok (test)
    if (!ok) @panic("test failure");
             ^
src/unittest.zen:9:11: 0x20462a 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/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: []const T, actual: []const T) void

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

examples/ch05-testing/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 });
}

equal

fn equal(expected: var, actual: @typeOf(expected)) void

2つの引数を取り、その一致比較を行います。2つの引数が一致していれば成功、一致していなければ失敗となります。

第一引数に期待値、第二引数に実際の値を与えます。equalはジェネリックな関数になっており、様々な型に対して使うことが可能です。2つの引数は型が同一である必要があります。また、引数の型はコンパイル時計算可能である必要があります。

以下に示すequalは全て成功します。

examples/ch05-testing/src/unittest.zen:14:39

const equal = std.testing.equal;
test "equal" {
    // ブール型の比較
    equal(true, true);
    // comptime_int型の比較
    equal(1, 1);
    // u32型の比較
    equal(u32(1), 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);
}

err

fn err(expected_error: anyerror, actual_error_union: var) void

期待値にエラー型のエラー種別を、実際の値にエラー共用体を、それぞれ引数に取り、エラー共用体の値がエラー型でかつ期待値通りのエラー種別であるかどうか、検証します。

examples/ch05-testing/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);
}

依存ソースファイルのテスト

Zenでは、依存するソースファイルに含まれるテストも自動的に抽出します。新しくanother_unittest.zenというファイルを次の内容で作成して下さい。

examples/ch05-testing/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/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/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-2019 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.