随時更新
Criterionとは
C/C++言語用のユニットテストフレームワーク
- 公式
- https://github.com/Snaipe/Criterion
- 公式ドキュメント
- http://criterion.readthedocs.org/en/master/
- 本記事中のテストコード等
- https://github.com/thombashi/CriterionSample
Instllation
バイナリからインストール
https://github.com/Snaipe/Criterion/releases
今回はLinux版(debian8で実行)をインストールする。
OSX/Windows版用のバイナリもある。
$ wget https://github.com/Snaipe/Criterion/releases/download/v2.2.0/criterion-v2.2.0-linux-x86_64.tar.bz2
$ tar xvf criterion-v2.2.0-linux-x86_64.tar.bz2
$ ls criterion-v2.2.0/
include/ lib/ share/
$ mv criterion-v2.2.0/include/criterion/ /usr/include/
$ mv criterion-v2.2.0/lib/libcriterion.so /usr/lib/
ソースからインストール
ソースからビルドしてインストールする場合は下記を参照。
http://criterion.readthedocs.org/en/master/setup.html
Sampleを動かす
テストコード
# include <criterion/criterion.h>
Test(misc, failing) {
cr_assert(0);
}
Test(misc, passing) {
cr_assert(1);
}
コンパイル
コンパイル時にCriterionライブラリをリンクする。
gcc -o test simple.c -lcriterion
テスト実行
実行ファイルを起動すればテストが走る。
実行ファイルにはデフォルトでオプションが付与される。
./test -h
Tests compiled with Criterion v2.2.0
usage: ./test OPTIONS
options:
-h or --help: prints this message
-q or --quiet: disables all logging
-v or --version: prints the version of criterion these tests have been linked against
-l or --list: prints all the tests in a list
-jN or --jobs N: use N concurrent jobs
-f or --fail-fast: exit after the first failure
--ascii: don't use fancy unicode symbols or colors in the output
-S or --short-filename: only display the base name of the source file on a failure
--pattern [PATTERN]: run tests matching the given pattern
--tap[=FILE]: writes TAP report in FILE (no file or "-" means stderr)
--xml[=FILE]: writes XML report in FILE (no file or "-" means stderr)
--always-succeed: always exit with 0
--no-early-exit: do not exit the test worker prematurely after the test
--verbose[=level]: sets verbosity to level (1 by default)
-OP:F or --output=PROVIDER=FILE: write test report to FILE using the specified provider
$ ./test
[----] simple.c:4: Assertion failed: The expression 0 is false.
[FAIL] misc::failing: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 1 | Failing: 1 | Crashing: 0
アサーション
アサーション用の関数一覧は以下に記載されている。
http://criterion.readthedocs.org/en/master/assert.html
assert/expectの違い
アサーション関数一覧にはcr_assert_xx
のみ記載されているが、assert
の箇所のみexpect
となった
cr_expect_xx
もある(公式にも説明あり)。両者の違いは以下。
- assert: 条件にかかった時点でテストを止める
- expect: 条件にかかってもテストを継続する
assert/expectのsample
https://github.com/Snaipe/Criterion/blob/bleeding/samples/asserts.c
より一部抜粋。
# include <criterion/criterion.h>
Test(asserts, base) {
cr_expect(false, "assert is fatal, expect isn't");
cr_assert(false, "This assert runs");
cr_assert(false, "This does not");
}
cr_<assert/expect>_xxx
関数の引数の最後はフォーマット文字列となっている(全関数共通)。
指定したフォーマット文字列はアサーションに引っかかると出力される(かからない場合は何も出ない)。
これを実行すると、以下の結果となる。
./test
[----] assert.c:4: Assertion failed: assert is fatal, expect isn't
[----] assert.c:5: Assertion failed: This assert runs
[FAIL] asserts::base: (0.00s)
[====] Synthesis: Tested: 1 | Passing: 0 | Failing: 1 | Crashing: 0
cr_expect(false, "assert is fatal, expect isn't")
はassertが出るが、継続する。
cr_assert(false, "This assert runs")
でもassertが出て、ここで止まる。
cr_assert(false, "This does not")
は実行されない。
別々のテストにした場合
# include <criterion/criterion.h>
Test(asserts, first) {
cr_expect(false, "assert is fatal, expect isn't");
}
Test(asserts, second) {
cr_assert(false, "This assert runs");
}
Test(asserts, third) {
cr_assert(false, "This does not");
}
↑のようにテスト毎に分けた場合は全て実行される。
./test --verbose
[----] Criterion v2.2.0
[====] Running 3 tests from asserts:
[RUN ] asserts::first
[----] assertion.c:4: Assertion failed: assert is fatal, expect isn't
[FAIL] asserts::first: (0.00s)
[RUN ] asserts::second
[----] assertion.c:8: Assertion failed: This assert runs
[FAIL] asserts::second: (0.00s)
[RUN ] asserts::third
[----] assertion.c:12: Assertion failed: This does not
[FAIL] asserts::third: (0.00s)
[====] Synthesis: Tested: 3 | Passing: 0 | Failing: 3 | Crashing: 0
テスト結果出力
詳細出力(--verbose
)
テスト実行時に--verbose
オプションを付けて実行すると、個々のテスト結果が出力される。
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from misc:
[RUN ] misc::passing
[RUN ] misc::failing
[PASS] misc::passing: (0.00s)
[----] /home/tmp/criterion/sample/simple.c:4: Assertion failed: The expression 0 is false.
[FAIL] misc::failing: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 1 | Failing: 1 | Crashing: 0
--verbose[=level]: sets verbosity to level (1 by default)
とあるが、
値を変えても出力に差は見られなかった。
XML出力(--xml
)
テスト実行時に--xml
オプションを付けて実行することで、
JInit XML形式で結果出ができる。
--xml
だけで実行した場合は標準エラー出力に出力される。
--xml=<file path>
として実行すると、XMLを指定ファイルに出力できる。
Jenkins等のCIツールと連携させるときに有用そう。
$ ./test --xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Tests compiled with Criterion v2.2.0 -->
<testsuites name="Criterion Tests" tests="2" failures="1" errors="0" disabled="0">
<testsuite name="misc" tests="2" failures="1" errors="0" disabled="0" skipped="0">
<testcase name="passing" assertions="1" status="PASSED">
</testcase>
<testcase name="failing" assertions="1" status="FAILED">
<failure type="assert" message="1 assertion(s) failed.">simple.c:4: The expression 0 is false. </failure>
</testcase>
</testsuite>
</testsuites>
テスト実行順
デフォルトで実行すると、並列でテストが実行される。
(実行毎にテストログの出力順が変わる)
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from misc:
[RUN ] misc::failing
[----] simple.c:4: Assertion failed: The expression 0 is false.
[FAIL] misc::failing: (0.00s)
[RUN ] misc::passing
[PASS] misc::passing: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 1 | Failing: 1 | Crashing: 0
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from misc:
[RUN ] misc::failing
[RUN ] misc::passing
[PASS] misc::passing: (0.00s)
[----] simple.c:4: Assertion failed: The expression 0 is false.
[FAIL] misc::failing: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 1 | Failing: 1 | Crashing: 0
--no-early-exit
オプションと-j1
オプションを付けて実行すると、実行順が実行毎に同じになる。
./test --verbose -j1 --no-early-exit
[----] Criterion v2.2.0
[====] Running 2 tests from misc:
[RUN ] misc::failing
[----] simple.c:4: Assertion failed: The expression 0 is false.
[FAIL] misc::failing: (0.00s)
[RUN ] misc::passing
[PASS] misc::passing: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 1 | Failing: 1 | Crashing: 0
個人的に開発中はログが順番に出た方が嬉しいので以下のような起動スクリプトを使う。
# !/bin/sh
TEST_BIN="./test"
$TEST_BIN --verbose -j1 --no-early-exit --xml=result.xml
異常系テスト
プログラム中で異常終了する場合をテストする方法。
公式ではこのあたり。
http://criterion.readthedocs.org/en/master/starter.html
例として、以下のような和が0だったらexit
、負だったらabort
するような
恐ろしいdangerous_add
関数があったとして、これをテストする。
int dangerous_add(int lhs, int rhs);
# include <stdlib.h>
# include "dangerous_add.h"
int dangerous_add(int lhs, int rhs)
{
int sum;
sum = lhs + rhs;
if (sum == 0)
exit(1);
if (sum < 0)
abort();
return sum;
}
テストコードは以下のあたり
https://github.com/thombashi/CriterionSample/tree/master/test_abnormal
exitする場合のテスト(.exit_code
)
テストの.exit_code
に期待する終了コードを指定する。
実際の終了コードと一致していればテスト成功となる。
.exit_code
成功例
# include <criterion/criterion.h>
# include "dangerous_add.h"
Test(dangerous_add, test_exit, .exit_code=1) {
dangerous_add(1, -1);
}
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from dangerous_add:
[RUN ] dangerous_add::hoge
[RUN ] dangerous_add::test_exit
[PASS] dangerous_add::test_exit: (0.00s)
[PASS] dangerous_add::hoge: (0.00s)
[====] Synthesis: Tested: 2 | Passing: 2 | Failing: 0 | Crashing: 0
.exit_code
失敗例
指定した.exit_code
が実際の終了コードと異なっていたり、
.exit_code
が指定されていないテストでexit
で終了したりするとテスト失敗となる。
# include <criterion/criterion.h>
# include "dangerous_add.h"
Test(dangerous_add, test_fail_1, .exit_code=2, .description="expected exit code == 2") {
dangerous_add(1, -1);
}
Test(dangerous_add, test_fail_2, .description="without '.exit_code'") {
dangerous_add(1, -1);
}
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from dangerous_add:
[RUN ] dangerous_add::test_fail_1
[RUN ] expected exit code == 2
[RUN ] dangerous_add::test_fail_2
[RUN ] without '.exit_code'
[FAIL] dangerous_add::test_fail_1: (0.00s)
[----] Warning! The test `dangerous_add::test_fail_2` exited during its setup or teardown.
[====] Synthesis: Tested: 2 | Passing: 0 | Failing: 2 | Crashing: 1
.description
を指定するとテスト中に出力される。
signalを投げる場合のテスト(.signal
)
テストの.signal
に期待するsignalを指定する。
実際のsignalと一致していればテスト成功となる。
.signal
成功例
# include <signal.h>
# include <criterion/criterion.h>
# include "dangerous_add.h"
Test(dangerous_add, test_passed, .signal=SIGABRT, .description="expected signal == SIGABRT") {
dangerous_add(-1, -1);
}
$ ./test --verbose
[----] Criterion v2.2.0
[====] Running 1 test from dangerous_add:
[RUN ] dangerous_add::test_passed
[RUN ] expected signal == SIGABRT
[PASS] dangerous_add::test_passed: (0.00s)
[====] Synthesis: Tested: 1 | Passing: 1 | Failing: 0 | Crashing: 0
.signal
失敗例
指定した.signal
が実際のsignalと異なっていたり、
.signal
が指定されていないテストでsignal投げて終了したりするとテスト失敗となる。
# include <signal.h>
# include <criterion/criterion.h>
# include "dangerous_add.h"
Test(dangerous_add, test_fail_1, .signal=SIGSEGV, .description="expected signal == SIGSEGV") {
dangerous_add(-1, -1);
}
Test(dangerous_add, test_fail_2, .description="without '.signal'") {
dangerous_add(-1, -1);
}
./test --verbose
[----] Criterion v2.2.0
[====] Running 2 tests from dangerous_add:
[RUN ] dangerous_add::test_fail_2
[RUN ] without '.signal'
[RUN ] dangerous_add::test_fail_1
[RUN ] expected signal == SIGSEGV
[FAIL] dangerous_add::test_fail_1: (0.00s)
[----] /home/criterion/sample/abnormal/test_signal.c:16: Unexpected signal caught below this line!
[FAIL] dangerous_add::test_fail_2: CRASH!
[====] Synthesis: Tested: 2 | Passing: 0 | Failing: 2 | Crashing: 1
Parameterized tests
あるパラメータに色々な値を与えてテストを実行したい場合に使う。
- 公式ドキュメント
- Using parameterized tests — Criterion 2.2.0 documentation
- http://criterion.readthedocs.org/en/master/parameterized.html
- 公式サンプル
- https://github.com/Snaipe/Criterion/blob/bleeding/samples/parameterized.c
以下、簡単なプログラムで説明する。
テスト対象プログラム
2値の和を返すだけのプログラムをテストする。
https://github.com/thombashi/CriterionSample/tree/master/test_parameterize
int add(int lhs, int rhs);
# include "add.h"
int add(int lhs, int rhs)
{
return lhs + rhs;
}
# include <stdio.h>
# include <criterion/parameterized.h>
# include "add.h"
struct parameter_tuple {
int lhs;
int rhs;
int expected;
};
ParameterizedTestParameters(parameterized_test, add) {
static struct parameter_tuple params[] = {
{1, 1, 2},
{-1, -1, -2},
{-100, 200, 100},
};
return cr_make_param_array(
struct parameter_tuple,
params, sizeof (params) / sizeof (struct parameter_tuple));
}
ParameterizedTest(struct parameter_tuple *tup, parameterized_test, add) {
int result;
fprintf(stdout,
"Test: lhs=%d, rhs=%d, expected=%d\n",
tup->lhs, tup->rhs, tup->expected);
fflush(stdout);
result = add(tup->lhs, tup->rhs);
cr_expect_eq(
result, tup->expected,
"Failed: lhs=%d, rhs=%d, result=%d, expected=%d\n",
tup->lhs, tup->rhs, result, tup->expected);
}
説明
struct parameter_tuple
パラメータの組を定義する構造体。
ParameterizedTestParameters(parameterized_test, add)
パラメータの値の組み合わせを定義する。parameterized_test, add
がテスト識別子となる
(前者がtest suite名、後者がテスト名)。
テスト識別子で対になるParameterizedTest
と結びつく。
return cr_make_param_array(<パラメータ構造体>, <パラメータ配列へのポインタ>, <パターン数>)
ParameterizedTestParametersの返り値とする。引数の意味は↑。
ParameterizedTest(struct parameter_tuple *tup, parameterized_test, add)
テスト記述部分。
struct parameter_tuple *tup
はテストに与えられる引数。
ParameterizedTestParameters
で定義したパラメータ配列が1つずつ渡される。
parameterized_test, add
はParameterizedTestParameters
と結びつけるためのテスト識別子。
expect/assertは通常のテストと同等のものが使用できる。
テスト実行結果
$ ./test --verbose --no-early-exit -j1
[----] Criterion v2.2.0
[====] Running 1 test from parameterized_test:
[RUN ] parameterized_test::add
Test: lhs=1, rhs=1, expected=2
[PASS] parameterized_test::add: (0.00s)
[RUN ] parameterized_test::add
Test: lhs=-1, rhs=-1, expected=-2
[PASS] parameterized_test::add: (0.00s)
[RUN ] parameterized_test::add
Test: lhs=-100, rhs=200, expected=100
[PASS] parameterized_test::add: (0.00s)
[----] Writing xml report in `result.xml`.
[====] Synthesis: Tested: 3 | Passing: 3 | Failing: 0 | Crashing: 0
留意点
cr_assert_xxx
Parameterize testでassertにかかった場合でも、後続のパラメータのテストは継続する。
XML出力
Parameterize testでXML出力すると、成功時にはテストケースに対応するパラメータがわからない。
(失敗時はcr_assert/expect_xxxの引数に指定したフォーマット文字列が出力される)。
成功時にも出力する方法があるのかもしれない。
./test --verbose --no-early-exit -j1 --xml=result.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Tests compiled with Criterion v2.2.0 -->
<testsuites name="Criterion Tests" tests="3" failures="0" errors="0" disabled="0">
<testsuite name="parameterized_test" tests="3" failures="0" errors="0" disabled="0" skipped="0">
<testcase name="add" assertions="1" status="PASSED">
</testcase>
<testcase name="add" assertions="1" status="PASSED">
</testcase>
<testcase name="add" assertions="1" status="PASSED">
</testcase>
</testsuite>
</testsuites>
同じテストをcr_expect_eq
からcr_expect_neq
にして、わざと失敗させる。
すると、テストケースの出力にfailure
タグが追加され、パスと標準出力が追加される。
<?xml version="1.0" encoding="UTF-8"?>
<!-- Tests compiled with Criterion v2.2.0 -->
<testsuites name="Criterion Tests" tests="3" failures="3" errors="0" disabled="0">
<testsuite name="parameterized_test" tests="3" failures="3" errors="0" disabled="0" skipped="0">
<testcase name="add" assertions="1" status="FAILED">
<failure type="assert" message="1 assertion(s) failed.">test_parameterize.c:35: Failed: lhs=-100, rhs=200, result=100, expected=100 </failure>
</testcase>
<testcase name="add" assertions="1" status="FAILED">
<failure type="assert" message="1 assertion(s) failed.">test_parameterize.c:35: Failed: lhs=-1, rhs=-1, result=-2, expected=-2 </failure>
</testcase>
<testcase name="add" assertions="1" status="FAILED">
<failure type="assert" message="1 assertion(s) failed.">test_parameterize.c:35: Failed: lhs=1, rhs=1, result=2, expected=2 </failure>
</testcase>
</testsuite>
</testsuites>
組み合わせテスト(Theory)
パラメータの値の組み合わせでテストを実行する。
標準でParameterize testやTheoryのような機能があるのは結構便利。
- 公式ドキュメント
- Using theories — Criterion 2.2.0 documentation
- http://criterion.readthedocs.org/en/master/theories.html
- 公式サンプル
- https://github.com/Snaipe/Criterion/blob/bleeding/samples/theories.c
テスト対象プログラム
uint64_t dangerous_add_2(int lhs, int rhs)
{
if (lhs < 0)
exit(1);
if (rhs < 0)
abort();
return lhs + rhs;
}
# include <stdio.h>
# include <stdint.h>
# include <limits.h>
# include <criterion/theories.h>
# include "dangerous_add.h"
TheoryDataPoints(theory_test, passing) = {
DataPoints(int, 0, -1, 1, INT_MAX, INT_MIN),
DataPoints(int, 0, -1, 1, INT_MAX, INT_MIN),
};
Theory((int lhs, int rhs), theory_test, passing) {
uint64_t result;
cr_assume_gt(lhs, 0);
cr_assume_gt(rhs, 0);
fprintf(stdout,
"Test: lhs=%d, rhs=%d\n",
lhs, rhs);
fflush(stdout);
result = dangerous_add_2(lhs, rhs);
cr_assert_gt(
result, 0,
"Failed: lhs=%d, rhs=%d, result=%d\n",
lhs, rhs, result);
}
説明
TheoryDataPoints(theory_test, passing)
組み合わせパラメータを定義する。theory_test, passing
がテスト識別子となる
(前者がtest suite名、後者がテスト名)。
テスト識別子で対になるTheory
と結びつく。
DataPoints(int, 0, -1, 1, INT_MAX, INT_MIN)
一つのパラメータの型と、取り得る値。
Theory((int lhs, int rhs), theory_test, passing)
テスト記述部分。
最初の(int lhs, int rhs)
は個々のテストに与えられる引数。
theory_test, passing
はTheoryDataPoints
と結びつけるためのテスト識別子。
cr_assume_gt(lhs, 0);
テスト実行の前提条件。これを満たさないパラメータの組み合わせはスキップされる。
上記はlhs
が0以下の場合はスキップする、という意味。
cr_assume_gt
以外にもcr_assume_xxx
は色々種類がある。
詳細は公式を参照。
expect/assertは通常のテストと同等のものが使用できる。
テスト実行結果
$./run_test.sh
[----] Criterion v2.2.0
[====] Running 1 test from theory_test:
Test: lhs=1, rhs=1
Test: lhs=2147483647, rhs=1
Test: lhs=1, rhs=2147483647
Test: lhs=2147483647, rhs=2147483647
[RUN ] theory_test::add
[PASS] theory_test::add: (0.00s)
[----] Writing xml report in `result.xml`.
[====] Synthesis: Tested: 1 | Passing: 1 | Failing: 0 | Crashing: 0
パラメータの値としては、マイナスとなる値もあるが、
cr_assume_gt
によってスキップされ、lhs
/rhs
の両方が
正となるケースのみテストが実施される。
Parameterize testとの使い分け
Theoryはsmoke testや成否判定が単純(BOOL値が戻り値の場合や異常系等)な値のテストに向いている。
Theoryはその性質上、結果の確認等、パラメータに対応する処理がやりにくい。
※Parameterize testで出来たような入力値に対する出力チェック等はTheoryではできない
公式にも説明があるのでそちらも。
cmake用雛形
cmake_minimum_required(VERSION 2.8)
set(SOURCE_ROOT "path to the source dir")
include_directories(
${SOURCE_ROOT}/include
)
add_executable(test
simple.c
)
target_link_libraries(test
criterion
)