2021/03/28

[c/c++]fffでテストを作る

前回の続き。

hiro99ma blog: [c/c++]久々のfff
https://hiro99ma.blogspot.com/2021/03/ccfff.html

 

何でもいいからmeekrosoft/fffを使ってテストを作ってみよう。


fffはgtest(おそらくgoogle testの古いやつ)が入っているので、まずはfakeの部分は忘れてテストを実行するところだけやる。

main関数はgtest側にあるので、テストされる方のファイルはmain()だけは別のファイルにするようにしておくのが良いだろう。まあこれは他のテストを使う場合でも同じかもしれん。

 

hello.h

01: #ifndef HELLO_H__
02: #define HELLO_H__
03: 
04: const char *get_hello(void);
05: 
06: #endif /* HELLO_H__ */

hello.c

01: #include "hello.h"
02: 
03: const char *get_hello(void)
04: {
05:     return "hello";
06: }
  

main.c

01: #include <stdio.h>
02: #include "hello.h"
03: 
04: int main(void)
05: {
06:     printf("%s\n", get_hello());
07:     return 0;
08: }
  

 

Makefile

01: hello:
02:     gcc -Wall -o hello hello.c main.c
03: 
04: clean:
05:     rm -f *.o hello
  

 

こんなファイルたちがあって、get_hello()が"hello"を返すかどうかというテストにしてみよう。

 

testsディレクトリをつくる

なくてもよいけど、本体とテストは別にしておきたいので、tests/というディレクトリを作る。

ついでに、tests/fffというディレクトリも作っておく。

 

fffのファイルをコピーする

meekrosoft/fffをcloneして、いくつかのファイルを作成した tests/fff/ ディレクトリの中に置く。

    • gtestディレクトリまるまる
    • fff.h
    • LICENSE

全部コピーしても良いのだが、最低限いるのはこれらだ。

 

テスト本体

gtest.hを見る限り、テストで使える比較マクロはこれらのようだ。

//    * {ASSERT|EXPECT}_EQ(expected, actual): Tests that expected == actual
//    * {ASSERT|EXPECT}_NE(v1, v2):           Tests that v1 != v2
//    * {ASSERT|EXPECT}_LT(v1, v2):           Tests that v1 < v2
//    * {ASSERT|EXPECT}_LE(v1, v2):           Tests that v1 <= v2
//    * {ASSERT|EXPECT}_GT(v1, v2):           Tests that v1 > v2
//    * {ASSERT|EXPECT}_GE(v1, v2):           Tests that v1 >= v2

と言いつつ、文字列とboolもあるようだ。stdbool.hがいるのかな? そこは試してない。

{ASSERT|EXPECT}_TRUE(v1)
{ASSERT|EXPECT}_FALSE(v1)

{ASSERT|EXPECT}_STREQ(v1, v2)
{ASSERT|EXPECT}_STRNE((v1, v2)
{ASSERT|EXPECT}_STRCASEEQ((v1, v2)
{ASSERT|EXPECT}_STRCASENE((v1, v2)

FLOATもあったし、他にもいろいろありそうだ。

ASSERTとEXPECTの違いは、失敗時に関数内の処理を継続するかどうか。ASSERTで失敗するとそこでreturnして次のテスト関数に進むし、EXPECTで失敗すると次の行以降も実行する(もちろんテストとしてはエラーになるが)。
個人的にはASSERTだけでいいんじゃないのかと思うが、まあこれは内容次第だろう。

 

tests/test_hello.cpp

01: #include "fff/gtest/gtest.h"
02: 
03: extern "C" {
04:     #include "../hello.c"
05: }
06: 
07: class test_hello: public testing::Test {
08: };
09: 
10: TEST_F(test_hello, hello1)
11: {
12:     ASSERT_STREQ(get_hello(), "hello");
13: }
14: 
15: TEST_F(test_hello, hello2)
16: {
17:     ASSERT_STRNE(get_hello(), "hellos");
18: }
19: 
20: TEST_F(test_hello, hello3)
21: {
22:     ASSERT_STRCASEEQ(get_hello(), "HELLO");
23: }

Makefile

ここはcmakeとか他にもいろいろあるかもしれんし、シェルスクリプトでもいいのかもしれん。
自分の好みで。

01: TEST_SOURCES += \
02:     test_hello.cpp
03: 
04: all: mk_fff
05:     mkdir -p build
06:     g++ -o build/tst $(TEST_SOURCES) fff/build/*.o -pthread
07: 
08: mk_fff:
09:     mkdir -p fff/build
10:     cd fff/gtest; $(MAKE) all
11: 
12: test:
13:     ./build/tst
14: 
15: clean: 
16:     cd fff/gtest; $(MAKE) clean
17:     rm -rf fff/build build

 

ビルド&テスト

makeしてmake testすればテストが実行される。

$ make test
./build/tst
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from test_hello
[ RUN      ] test_hello.hello1
[       OK ] test_hello.hello1 (0 ms)
[ RUN      ] test_hello.hello2
[       OK ] test_hello.hello2 (0 ms)
[ RUN      ] test_hello.hello3
[       OK ] test_hello.hello3 (0 ms)
[----------] 3 tests from test_hello (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (0 ms total)
[  PASSED  ] 3 tests.


さて、ここまではfff無しだった。
fffはfff.hだけあればよいのだが、いくつか準備がいる。

まず、fffが使うグローバル変数の確保。
includeは何箇所でやっても良いのだが、1箇所だけDEFINE_FFF_GLOBALSを置く。どこに置くか悩むんだったら専用のファイルを作ってしまえば良かろう。
置き忘れたら fffが無いというリンクエラーになるので、気付くだろう。

 

設定はそれだけで、あとはfake関数を作るだけだ。
マクロはこの2つ。

  • FAKE_VOID_FUNC : 戻り値なし
  • FAKE_VALUE_FUNC : 戻り値あり

VOIDかVALUEかは、戻り値があるかどうかだけだ。戻り値なしも同じマクロにできればよいのだが、有ると無しでは大きく違うので仕方なかろう。

書き方はFAKE_VOID_FUNC(funcname, args...)か、FAKE_VALUE_FUNC(戻り値の型, funcname, args...)で、argsは引数の型だけ。引数が無ければ書かなくて良い。

どうやってマクロで引数を処理しているかというと、FUNC0、FUNC1、...、FUNC20、のようにそれぞれの引数のマクロが用意されているのだった。大変ですな。。。

fake関数を作ると、関数名の後ろに_fakeを付けた構造体の変数が定義される。

  • call_count
  • arg_history_len
  • arg_histories_dropped

他にもある。。。が、私が使うのはcall_countくらいか。

戻り値を変更したい場合は、だいたいcustom_fakeを使っている。return_valやSET_RETURN_SEQ()もあるが、使い分けるのが面倒なので、自分で関数を作ってやってしまうことが多い。そういう関数はclassのstatic関数として作っている(こんな感じ)。gccだとnested functionsなるものに対応しているので、関数の中にそこで使うカスタム関数を定義しても良いそうだ。まあ、テスト用だから互換性とかそんなに気にしなくてもよいだろうしね。

call_countなんかは蓄積するので、クリアしたければResettingするとよかろう。書いてあるようにgtestの setUp()に仕込んで毎回クリアするといいんじゃないかね。


あとは、やりたいことがこれでできなかったらREADMEを読む、くらいか。

他のC言語用テストフレームワークを使ったことがないので比較はできないのだが、インストールもいらないし、valgrindと併用してメモリリークを見つけたりもできるし、私は気に入っている。

0 件のコメント:

コメントを投稿

コメントありがとうございます。
スパムかもしれない、と私が思ったら、
申し訳ないですが勝手に削除することもあります。