2016/08/25

[c/c++]私のC言語 (5) - FFF

私は、だいたい実装するとユニットテストをしている。

実機ではなかなか通しにくいルートだけど、通ったときの動作は見ておきたい、という場合によいし、コードを書き換えた後でテストを通し、前との違いを比較する場合にも良い。

テストを作るのは面倒で時間はかかるのだが、自分の書いたものが期待通りかどうかを知るためにもやっておきたいものだ。

gccはカバレッジを取るためのビルドができるので、通ったことがないかどうかを目視しやすい。
一度通ったルートはカウントアップされるだけなので、このときにここを通った、というのには向かないのだが、それを知りたければ1つだけテストを書いて実行すれば良いだけだ。


意図通りのルートを通したいとなると、標準関数やドライバも意図通りの値を返してほしい。
が、それは難しいので、スタブやらモックやらを作る。
私は、それをFFFというフレームワークを使ってやっている。

meekrosoft/fff: A testing micro framework for creating function test doubles

検索して探すときは「fff fake」などで出てくるだろう。
使っている人の例もあるので、そっちの方がわかりやすいかも。

 


まず、githubからcloneしよう。
今の最新は3cd33edだ。
私の環境は、cygwinである。

こんなソースを書いたとしよう。
ソースファイルべた書きで済まぬ。。。

my_open(), my_close(), my_access()の3つがある。
my_open()とmy_close()UARTのオープン/クローズのイメージ。
my_access()は、コマンドを送信するとレスポンスが返ってくるデバイスがつながっていて、1回のコマンド送信に対してレスポンスを2回に分けてチェックしながら受信する、という想定だ。
ファイルディスクリプタは、static変数sfdで保持しておく。


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static int sfd = -1;

int my_open(const char *pTty)
{
    if (sfd != -1) {
        return -100;
    }

    int fd = open(pTty, O_RDWR);
    if (fd == -1) {
        return -1;
    }
    sfd = fd;
    return sfd;
}

void my_close(void)
{
    close(sfd);
    sfd = -1;
}

int my_access(void)
{
    const uint8_t WRT[] = { 0x00, 0x01, 0x02, 0x03, 0x04 };

    uint8_t buf[20];
    ssize_t sz;
   
    sz = write(sfd, WRT, sizeof(WRT));
    if (sz != sizeof(WRT)) {
        return -2;
    }

    sz = read(sfd, buf, 5);
    if ((sz != 5) || (buf[4] != 0xff)) {
        return -3;
    }
    sz = read(sfd, &buf[5], 3);
    if ((sz != 3) || (buf[7] != 0x35)) {
        return -4;
    }

    return 0;
}


まず、my_open()のテストをするとしよう。

最初にsfdが-1以外かどうかをチェックして、オープン済みなら終わらせる。
次にopen()を呼んで、戻り値が-1だったら、エラーとして終わる。
それ以外なら、sfdに保存して終わる。

というわけで、ここでやりたいのは、

  • sfdを-1以外にして、-100が返ってくるのを確認する
  • sfdを-1にして、open()が-1を返すようにし、-1が返ってくるのを確認する
  • sfdを-1にして、open()が-1以外を返すようにし、その値が返ってくるのを確認する

という3つだ。
これを、FFFを使ったテストコードにすると、こうなる。

TEST_F(test, open_1)
{
    sfd = 3;

    int fd = my_open("/dev/ttyS1");

    ASSERT_EQ(-100, fd);
}

TEST_F(test, open_2)
{
    sfd = -1;

    open_fake.return_val = -1;

    int fd = my_open("/dev/ttyS1");

    ASSERT_EQ(-1, fd);
}

TEST_F(test, open_3)
{
    sfd = -1;

    open_fake.return_val = 5;

    int fd = my_open("/dev/ttyS1");

    ASSERT_EQ(5, fd);
}

長くなるので省略しているが、これよりも前の部分がある。
実はそこで、テスト対象のソースファイルをincludeしている。
だから、直接sfdが扱えるのだ。
なんとなく邪道じゃろう?
でも、楽なのだよ。

open_fake.return_val、というのは、関数をモック化する手続きをしておくと、<関数名>_fakeという構造体ができるようになっていて、そのreturn_valという変数だ。
これに値を入れておくと、その値が戻り値になってくれる。

ASSERT_EQ()は、FFFではなくGoogle Testが提供するものだ。
FFFはGoogle Testと併用しやすくなっている。


では、my_access()みたいにread()を何度も呼び出すタイプはどうするか?
いくつかやりかたはあるのだが、戻り値も変える、引数で返す値も返るとなると、自分でモックを定義した方が簡単だと思っている。

たとえば、戻り値が0になるテストだと、こういう感じだ。

TEST_F(test, access_1)
{
    sfd = 5;

    class dummy {
    public:
        static ssize_t read(int fd, void *p, size_t sz) {
            static int count = 0;
            int ret = -1;
            uint8_t* buf = (uint8_t *)p;
            switch (count) {
            case 0:
                ret = 5;
                buf[4] = 0xff;
                break;
            case 1:
                ret = 3;
                buf[2] = 0x35;
                break;
            default:
                assert(0);
            }
            count++;
            return ret;
        }
    };

    write_fake.return_val = 5;
    read_fake.custom_fake = dummy::read;

    int ret = my_access();

    ASSERT_EQ(0, ret);
}

read_fake.custom_fakeはデフォルトがNULLで、その場合はreturn_valの値を返す(配列で順番に値を返してもらうようにもできる)。
設定すると、その関数が呼ばれる。

今回は、dummyクラスのread()を呼ぶようにしている。
普通の関数にしても良いのだが、テストごとに定義していると名前の重複が発生して面倒になりそうだから、こういう形にしている。


今回作ったものは、ここに置いた。
https://github.com/hirokuma/fff_examples

作ったのは、func., test_func.cppと、Makefileをいじっただけだ。
それ以外のものは、FFFからコピーしただけである。

慣れるまでは使いづらいかもしれないが、いくつか作って自分の作り方を蓄積させておくと、だんだん楽になっていくかもしれない。

0 件のコメント:

コメントを投稿

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