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


随時更新

# 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版用のバイナリもある。

```console
$ 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を動かす
## テストコード
https://github.com/Snaipe/Criterion/blob/bleeding/samples/simple.c

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

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

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

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

```console:compile
gcc -o test simple.c -lcriterion
```

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

```text: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
```

```console:テスト実行結果
$ ./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: 条件にかかってもテストを継続する

http://criterion.readthedocs.org/en/master/starter.html#asserting-things

## assert/expectのsample
https://github.com/Snaipe/Criterion/blob/bleeding/samples/asserts.c
より一部抜粋。

```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`関数の引数の最後はフォーマット文字列となっている(全関数共通)。
指定したフォーマット文字列はアサーションに引っかかると出力される(かからない場合は何も出ない)。
これを実行すると、以下の結果となる。

```console
./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")`は実行されない。

## 別々のテストにした場合
```c
#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");
}
```

↑のようにテスト毎に分けた場合は全て実行される。

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

```console:--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:--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>
```

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

```console:デフォルト実行その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
```

```console:デフォルト実行その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`オプションを付けて実行すると、実行順が実行毎に同じになる。

```console
./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
```

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

```bash: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`関数があったとして、これをテストする。

```c:dangerous_add.h
int dangerous_add(int lhs, int rhs);
```

```c: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`成功例
```c:exit_code成功例
#include <criterion/criterion.h>
#include "dangerous_add.h"

Test(dangerous_add, test_exit, .exit_code=1) {
    dangerous_add(1, -1);
}
```

```console: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`で終了したりするとテスト失敗となる。

```c: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);
}
```

```console: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`成功例
```console: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);
}
```

```console: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投げて終了したりするとテスト失敗となる。

```c: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);
}
```

```console: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
あるパラメータに色々な値を与えてテストを実行したい場合に使う。

- 公式ドキュメント
 - 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

```c:header.h
int add(int lhs, int rhs);
```

```c:add.c
#include "add.h"

int add(int lhs, int rhs)
{
	return lhs + rhs;
}
```

```c: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, add`は`ParameterizedTestParameters`と結びつけるためのテスト識別子。

expect/assertは通常のテストと同等のものが使用できる。

## テスト実行結果
```bash
$ ./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の引数に指定したフォーマット文字列が出力される)。
成功時にも出力する方法があるのかもしれない。

```console:成功時のXML出力
./test --verbose --no-early-exit -j1 --xml=result.xml
```

```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: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のような機能があるのは結構便利。

- 公式ドキュメント
 - 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

## テスト対象プログラム
```c:dangerous_add.c
uint64_t dangerous_add_2(int lhs, int rhs)
{
    if (lhs < 0)
        exit(1);

    if (rhs < 0)
        abort();
        
    return lhs + rhs;
}
```

```c: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, passing`は`TheoryDataPoints`と結びつけるためのテスト識別子。

`cr_assume_gt(lhs, 0);`
テスト実行の前提条件。これを満たさないパラメータの組み合わせはスキップされる。
上記は`lhs`が0以下の場合はスキップする、という意味。
`cr_assume_gt`以外にも`cr_assume_xxx`は色々種類がある。
詳細は公式を参照。

expect/assertは通常のテストと同等のものが使用できる。

## テスト実行結果
```console
$./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: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
)
```