LoginSignup
22
18

More than 5 years have passed since last update.

C/C++ユニットテストフレームワークCriterionの使い方

Last updated at Posted at 2016-02-14

随時更新

Criterionとは

C/C++言語用のユニットテストフレームワーク

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を動かす

テストコード

simple.c
#include <criterion/criterion.h>

Test(misc, failing) {
    cr_assert(0);
}

Test(misc, passing) {
    cr_assert(1);
}

コンパイル

コンパイル時にCriterionライブラリをリンクする。

compile
gcc -o test simple.c -lcriterion

テスト実行

実行ファイルを起動すればテストが走る。
実行ファイルにはデフォルトでオプションが付与される。

help
./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
より一部抜粋。

assert.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オプションを付けて実行すると、個々のテスト結果が出力される。

--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ツールと連携させるときに有用そう。

--xmlオプション付き実行結果
$ ./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.&#10;</failure>
    </testcase>
  </testsuite>
</testsuites>

テスト実行順

デフォルトで実行すると、並列でテストが実行される。
(実行毎にテストログの出力順が変わる)

デフォルト実行その1
$ ./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
デフォルト実行その2
$ ./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

個人的に開発中はログが順番に出た方が嬉しいので以下のような起動スクリプトを使う。

run_test.sh
#!/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関数があったとして、これをテストする。

dangerous_add.h
int dangerous_add(int lhs, int rhs);
dangerous_add.c
#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成功例

exit_code成功例
#include <criterion/criterion.h>
#include "dangerous_add.h"

Test(dangerous_add, test_exit, .exit_code=1) {
    dangerous_add(1, -1);
}
exit_code成功例実行結果
$ ./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で終了したりするとテスト失敗となる。

exit_code失敗例
#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);
}
exit_code失敗例出力
$ ./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成功例

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);
}
signal成功例出力
$ ./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投げて終了したりするとテスト失敗となる。

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);
}
signal失敗例出力
./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

あるパラメータに色々な値を与えてテストを実行したい場合に使う。

以下、簡単なプログラムで説明する。

テスト対象プログラム

2値の和を返すだけのプログラムをテストする。
https://github.com/thombashi/CriterionSample/tree/master/test_parameterize

header.h
int add(int lhs, int rhs);
add.c
#include "add.h"

int add(int lhs, int rhs)
{
    return lhs + rhs;
}
test_parametize.c
#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, addParameterizedTestParametersと結びつけるためのテスト識別子。

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の引数に指定したフォーマット文字列が出力される)。
成功時にも出力する方法があるのかもしれない。

成功時のXML出力
./test --verbose --no-early-exit -j1 --xml=result.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タグが追加され、パスと標準出力が追加される。

result.xml(失敗時)
<?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&#10;</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&#10;</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&#10;</failure>
    </testcase>
  </testsuite>
</testsuites>

組み合わせテスト(Theory)

パラメータの値の組み合わせでテストを実行する。
標準でParameterize testやTheoryのような機能があるのは結構便利。

テスト対象プログラム

dangerous_add.c
uint64_t dangerous_add_2(int lhs, int rhs)
{
    if (lhs < 0)
        exit(1);

    if (rhs < 0)
        abort();

    return lhs + rhs;
}
test_theory.c
#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, passingTheoryDataPointsと結びつけるためのテスト識別子。

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用雛形

CMakeList.txt
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
)
22
18
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
22
18