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と併用してメモリリークを見つけたりもできるし、私は気に入っている。

[c/c++]久々のfff

普段、C言語のことは「clang」と書いていたのだが、LLVMのclangもあったんだということを思い出した。
まあそんなことはどうでもよくて、久々にC言語を使っている。
あんだけ使っていたのに、しばらく使わなかっただけで忘れてしまっているところがあるのは哀しいのだが、まあ加齢もあるから仕方あるまい。

 

で、今日は単体テストについてだ。
自分がどうやってやっていたのか思い出そうとしたところ、これが出てきた。

hiro99ma blog: [c/c++]私のC言語 (5) - FFF
https://hiro99ma.blogspot.com/2016/08/ccc-5-fff.html

モックを作る meekrosoft/fff と(これはmicrosoftと名前をかけてるのかな)、その中にgtestが入っている。gtestは google testのことか、あるいはその前身だろう。ファイルのヘッダを見ると2005年なのだが、GitHubには残っていなさそうだったのだ。今のgoogle testのgtest.hもファイルヘッダは2005年なのだが、fff/gtest/gtest.hとファイルサイズが違いすぎる。おそらく複数のファイルを1つにまとめてあるのだろう。

 


今のfffはv1.1が最新だ。

Release Release v1.1 · meekrosoft/fff
https://github.com/meekrosoft/fff/releases/tag/v1.1

というわけで、私のサンプルも更新した。といってもfff.hだけだが。

hirokuma/fff_examples: FFFを使った例
https://github.com/hirokuma/fff_examples

前回が2016年なのもあるが、fff.hの差分が多すぎて違いを見るのが難しい。

なぜか分からんが、gccのオプションに-pthreadが必要になってしまった。gtest.hでpthread関数が使われているためのようなんだけど、今回gtest.hは変わってないんだよなぁ。。。

# define GTEST_HAS_PTHREAD (GTEST_OS_LINUX || GTEST_OS_MAC || GTEST_OS_HPUX)

ああ、前回はcygwinで試したからこの条件に入っていなかったのか。納得だ。

 

テストを見ていて気付いたのだが、 weak_linkingサンプルはCソースしかない。
C++じゃないといけないと思い込んでいたのだが、いつしかC言語だけでテストが書けるようになっていたのか? でも、 __attribute__((weak)) とかMakefileに書いてあるし、なんか特殊なのかもしれん。

weakについてはこちらに書かれていた。

hiro99ma blog: [nrf51]app_error_handler()はWEAK
https://hiro99ma.blogspot.com/2015/02/nrf51apperrorhandlerweak.html

昔の私は言いました。

ライブラリ関数としてweakとしておけば、ユーザコードで上書きできるよ、みたいな感じらしい。

へー。
確かにMakefileを見ると、"FFF_GCC_FUNCTION_ATTRIBUTES"に"__attribute__((weak))"を設定していて、fff.hではFFF_GCC_FUNCTION_ATTRIBUTESを使ってFUNCNAMEを修飾している。
libfakes.aは、test内のDEFINE_FAKE_VALUE_FUNCみたいな関数をアーカイブしている。これらがweakになって置き換えられるようになっている、とういことか。
難しいこと考えるねぇ。

今までそういうのはcustom_fakeで行っていたと思う。
C++だとclassが使えて、そこにstaticな関数を作ることができるので、それをcustom_fakeにしてしまえば関数内で閉じたモックを作ることができるので便利だったのだ。
・・・と思っていたのだが、この例だと関数内に関数を書くことができてるな・・・そんなことできるんだっけ。nested functionをサポートしているならできるんだろう。

 

weakの件がC言語だけでテストコードまで書ける件と関係しているのかと思って見ていたのだが、関係はないのか?

別にC++でテストを作るのに困るわけでも無いし、どっちかといえば楽かもしれん。
ただ・・・テストコードは本体のCソースよりもだいたい量が多くなってしまう。そうするとGitHubの"Languages"でC++のプロジェクトのように見られかねないのだ。

image

まあいいんだけど、気になる場合は見直してみるのもよかろう。

 

うーん、今回は(も?)煮え切らない記事になってしまった。
次がんばろう。