Help us understand the problem. What is going on with this article?

Google Testで組み込みソフトをテストする

More than 1 year has passed since last update.

はじめに

今まで組み込み開発が中心でTDDしてこなかったのですが、テスト駆動開発による組み込みプログラミングを読んで、導入してみたいという気持ちが高まりました。
私はSTM32 Nucleo Boardの開発をしながら勉強しているので、これにTDDを適用できればと考えています。

テストツール

本で紹介されているのはUnityですが、Google Testを使用することにします。
導入は
Google Test / Google Mock
1分で実行できる?インストール不要、C言語向けGoogleTestサンプル for Linux
を参考にしました。
Google Testはここから1.8.0をダウンロードしました。

テスト作成

テストはTDDの場合には先に作成しておく必要がありませすが、今回はすでに作ってしまったコードをテストすることにします。
テスト対象はSTM32 Nucleo Boardでベアメタルなhello worldで作成したUSARTのドライバです。
USARTドライバはテストしやすいように、変更を行っています。(ソースコードはこちら)
受信待ちや送信待ちがあるため、通常のテストケースのみでは実装が難しくなります。
そのため、Mockを用いてHWに依存したIO実装の代役を実装します。
細かい文法は解説しきれないので、Google Testのマニュアルを参照してください。

test_usart.cpp
// テストケース記述ファイル
#include "gtest/gtest.h"
#include "gmock/gmock.h"

// テスト対象関数を呼び出せるようにするのだが
// extern "C"がないとCと解釈されない
extern "C" {
#include "usart_driver.h"
#include "stm32f303x8.h"
}

using ::testing::_;
using ::testing::Invoke;

class MockIo{
    public:
        MOCK_METHOD2(SetBit, void (__IO void*, uint32_t ));
        MOCK_METHOD2(ClearBit, void (__IO void*, uint32_t ));
        MOCK_METHOD2(ReadBit, uint32_t (__IO void*, uint32_t ));
        MOCK_METHOD1(ClearReg, void (__IO void* ));
        MOCK_METHOD2(WriteReg, void (__IO void*, uint32_t ));
        MOCK_METHOD1(ReadReg, uint32_t (__IO void* ));

        void FakeSetBit(__IO void* address, uint32_t bit){
            *((uint32_t*)address) |= bit;
        }

        void FakeClearBit(__IO void* address, uint32_t bit){
            *((uint32_t*)address) &= ~bit;
        }

        void FakeClearReg(__IO void* address){
            *((uint32_t*)address) = 0;
        }
        void FakeWriteReg(__IO void* address, uint32_t data){
            *((uint32_t*)address) = data;
        }

        void DelegateToVirtual() {
            ON_CALL(*this, SetBit(_, _)).WillByDefault(Invoke(this, &MockIo::FakeSetBit));
            ON_CALL(*this, ClearBit(_, _)).WillByDefault(Invoke(this, &MockIo::FakeClearBit));
            ON_CALL(*this, ClearReg(_)).WillByDefault(Invoke(this, &MockIo::FakeClearReg));
            ON_CALL(*this, WriteReg(_, _)).WillByDefault(Invoke(this, &MockIo::FakeWriteReg));
        }
};

MockIo *mock;

extern "C" {
void SetBit(__IO void* address, uint32_t data){
    mock->SetBit(address, data);
}

void ClearBit(__IO void* address, uint32_t data){
    mock->ClearBit(address, data);
}

void ReadBit(__IO void* address, uint32_t data){
    mock->ReadBit(address, data);
}

void ClearReg(__IO void* address){
    mock->ClearReg(address);
}

void WriteReg(__IO void* address, uint32_t data){
    mock->WriteReg(address, data);
}

uint32_t ReadReg(__IO void* address){
    return mock->ReadReg(address);
}
}

MockIoクラスはUSARTドライバが使用しているレジスタ設定用インターフェースio_reg.hを置き換えます。
テスト実行時はこちらが使用されるため、レジスタ設定やシーケンスが正しいかどうかを判定可能になります。
DelegateToVirtual()関数にはMockを呼び出したときに実行したい処理を記述しています。
例えば
ON_CALL(*this, SetBit(_, _)).WillByDefault(Invoke(this,&MockIo::FakeSetBit));
と書くと、SetBit呼び出し時に同時にFakeSetBitも呼び出されます。
Mockのみでは擬似レジスタ設定を保持することができないためこのようにしています。

次にテストケースを記述します。

test_usart.cppの続き
RCC_TypeDef *virtualRcc;
GPIO_TypeDef *virtualGpio;
USART_TypeDef *virtualUsart;

// UsartTestはテストケース群をまとめるグループ名のようなもの
class UsartTest : public ::testing::Test {
    protected:
        // グループ化されたテストケースはそれぞれのテストケース実行前に
        // この関数を呼ぶ。共通の初期化処理を入れておくとテストコードがすっきりする
        virtual void SetUp()
        {
            mock = new MockIo();
            // テスト環境はレジスタがメモリにマッピングされていないので擬似レジスタ領域を作成
            virtualRcc = new RCC_TypeDef();
            virtualGpio = new GPIO_TypeDef();
            virtualUsart = new USART_TypeDef();
            UsartCreate(virtualRcc, virtualGpio, virtualUsart);
        }
        // SetUpと同様にテストケース実行後に呼ばれる関数。共通後始末を記述する。
        virtual void TearDown()
        {
            delete mock;
            delete virtualRcc;
            delete virtualGpio;
            delete VirtualUsart;
        }
};

// テストケース
TEST_F(UsartTest, Init)
{
    mock->DelegateToVirtual();

    EXPECT_CALL(*mock, SetBit(_, _)).Times(6); //回数は問題ではないので微妙だがWarningがでるため
    EXPECT_CALL(*mock, ClearReg(_)).Times(3);
    EXPECT_CALL(*mock, WriteReg(_, _)).Times(1);

    UsartInit();

    EXPECT_EQ(RCC_AHBENR_GPIOAEN, virtualRcc->AHBENR & RCC_AHBENR_GPIOAEN);
    EXPECT_EQ(GPIO_MODER_MODER2_1|GPIO_MODER_MODER15_1, virtualGpio->MODER);
    EXPECT_EQ(0x700, virtualGpio->AFR[0]);
    EXPECT_EQ(0x70000000, virtualGpio->AFR[1]);
    EXPECT_EQ(RCC_APB1ENR_USART2EN, virtualRcc->APB1ENR);
    EXPECT_EQ(8000000L/115200L, virtualUsart->BRR);
    EXPECT_EQ(USART_CR1_RE|USART_CR1_TE|USART_CR1_UE, virtualUsart->CR1);
}

using ::testing::Return;

TEST_F(UsartTest, IsReadEnable)
{
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_RXNE)).WillOnce(Return(0));
    EXPECT_EQ(0, UsartIsReadEnable());
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_RXNE)).WillOnce(Return(USART_ISR_RXNE));
    EXPECT_EQ(USART_ISR_RXNE, UsartIsReadEnable());
}

TEST_F(UsartTest, IsWriteEnable)
{
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_TXE)).WillOnce(Return(0));
    EXPECT_EQ(0, UsartIsWriteEnable());
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_TXE)).WillOnce(Return(USART_ISR_TXE));
    EXPECT_EQ(USART_ISR_TXE, UsartIsWriteEnable());
}

TEST_F(UsartTest, Read)
{
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_RXNE)).WillRepeatedly(Return(0));
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_RXNE)).WillRepeatedly(Return(USART_ISR_RXNE));
    EXPECT_CALL(*mock, ReadReg(&virtualUsart->RDR)).WillRepeatedly(Return('a'));

    EXPECT_EQ('a', UsartRead());
}

TEST_F(UsartTest, Write)
{
    mock->DelegateToVirtual();

    char c = 's';

    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_TXE)).WillRepeatedly(Return(0));
    EXPECT_CALL(*mock, ReadBit(&virtualUsart->ISR, USART_ISR_TXE)).WillRepeatedly(Return(USART_ISR_TXE));
    EXPECT_CALL(*mock, WriteReg(&virtualUsart->TDR, c));

    UsartWrite(c);
    EXPECT_EQ(c, virtualUsart->TDR);
}

とりあえず、usart_driver.hの各インターフェースのテストを記述しています。
受信完了などHWがレジスタを設定する部分はEXPECT_CALLで戻り値を設定することで実現しています。

Makefile作成

ターゲット環境ではなくホスト環境でのビルドになります。
GTEST_DIRの位置にGoogle Testは置いておきます。
最初にmake gtest-genを行い、makeでテスト実行されます。

# Makefile
# gtest_main.ccはGoogleTestが用意してくれているmain関数、
# gmock-gtest-all.ccがGoogleTest全部入りファイルです
# -lpthreadをつけることにも注意。
# makeかmake allしたらビルドして実行まで行います。

TESTNAME = test_usart

GTEST_DIR = ../../../../../..
TEST_DIR = .
CODE_DIR = ..
INC_DIR = ../../..

INCLUDE = -I$(GTEST_DIR) -I$(INC_DIR)/include

SRCS = $(CODE_DIR)/usart_driver.c
OBJECTS = usart_driver.o

all: $(OBJECTS) $(TESTNAME)
    ./$(TESTNAME)

$(TESTNAME): $(OBJECTS)
    g++ -o $(TESTNAME) test_usart.cpp $(GTEST_DIR)/googletest/googletest/src/gtest_main.cc $(GTEST_DIR)/gmock-gtest-all.cc $(INCLUDE) -lpthread $(OBJECTS) -D DEBUG_GTEST

$(OBJECTS): $(SRCS)
    gcc -c $(SRCS) $(INCLUDE) -DEBUG_GTEST

clean:
    rm *.o $(TESTNAME)

gtest-gen:
    python $(GTEST_DIR)/googletest/googlemock/scripts/fuse_gmock_files.py $(GTEST_DIR) 

テスト実行

makeを実行すると次のようになります。
test.png
PASSしました。(PASSするように作っているから)

おわりに

テストケースに関しては経験不足でこれで十分なのかはよく分かりません。
しかし、テストは書けるようになったので、実際にTDDを取り入れて経験を積んでいこうと思います。
実機がないと確認できないことも多々あると思いますが、テストを考慮することで設計品質が高まることを期待しています。

mitazet
@求職中
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away