ファームウェアの単体テスト
Arduino 開発環境で真面目に単体テストするためにGoogleTestを導入する.
サンプルリポジトリは以下.
toms74209200/arduino_gtest_sample: C++ GoogleTest sample for Arduino farmware
環境
何でもかんでも VScode devcontainer で開発するのが個人的な流行り. 上記サンプルリポジトリでは docker と VScode だけで動く. devcontainer の使い方は調べれば出てくるので割愛.
コンテナ内の実際の開発環境では以下を使う.
- CMake
- clang
- clang-format
- clang-tidy
- arduino-cli
- GoogleTest
CMake, clang は GoogleTest を使うために利用する. clang-format はフォーマッター, clang-tidy はリンター(使ってない). フォーマッターとリンターは軽率に入れよう. ライブラリ等の導入のために arduino-cli も入れておく. うまくテスト対象部分を分離できていれば Arduino 用の開発環境はなくてもいいかもしれない.
詳しい導入方法は Dockerfile に書いてある.
RUN apt-get update && apt-get install -y \
git \
make \
cmake \
curl \
clang \
clang-format \
clang-tidy \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# install arduino-cli
RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh -s 0.21.1
COPY arduino-cli.yaml /root/.arduino15/arduino-cli.yaml
RUN arduino-cli core update-index \
&& arduino-cli core install arduino:avr
# install GoogleTest
RUN git clone https://github.com/google/googletest.git -b release-1.11.0 \
&& cd googletest \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make \
&& make install
単体テスト手法
単体テストができなくなってしまう原因は例えば Serial.print()
のような依存性や副作用だ. そのためこのような依存性をDIを使って分離してしまえば良い.
ディレクトリ構成
実装コードとテストコードを分離しつつ, Arduino できちんと読み込まれるようなディレクトリ構成にする. ちなみにサンプルリポジトリでは Google のC++スタイルガイドに則っているが, Arduino では .cc
ファイルが認識されないため, .cpp
としている.
arduino_gtest_sample/
├── CMakeLists.txt
├── arduino_gtest_sample.ino
├── build/
├── src/
│ ├── host_communication/
│ │ ├── host_communication.cpp
│ │ ├── host_communication.h
│ │ └── host_uart.h
│ ├── impl/
│ │ ├── host_uart_impl.cpp
│ │ ├── host_uart_impl.h
│ │ ├── sensor_uart_impl.cpp
│ │ └── sensor_uart_impl.h
│ └── sensor_communication/
│ ├── sensor_communication.cpp
│ ├── sensor_communication.h
│ └── sensor_uart.h
└── test/
├── host_communication/
│ ├── host_communication_test.cpp
│ └── host_uart_mock.h
└── sensor_communication/
├── sensor_communication_test.cpp
└── sensor_uart_mock.h
ディレクトリトップにメインファイルとなる .ino
ファイルと CmakeLists.txt
を配置する. テスト対象となる実装コードは src/
ディレクトリ下に配置する. 副作用を持っていてテストから除外するコードも src/
以下に置くが impl/
ディレクトリにまとめておき impl
をサフィックスとする. テストコードは test/
ディレクトリ以下に src/
と同じ構成で配置する. わかりやすいように test
をサフィックスとしてつける.
CmakeLists.txt
テストコードのビルドのため CMake を使用する. これを使うことで
CMakeList.txt
読み込み- ビルド
- テスト実行
の3ステップでテストできるようになる.
CMake なんもわからんが, テストコードのビルドのためにしか使わないのでそこまで考えずにテスト用にビルドプロジェクトを作れば良い. CMake でファームウェアのビルドするようなところはもうちょっとちゃんとした秘伝のタレがあるはず.
テスト対象の実装コードとテストコードは file
コマンドで探索する. このときテストから除外するコードは EXCLUDE
で除外する.
file(GLOB_RECURSE SRC_FILES RELATIVE /workspace src/*/*.cpp)
list(FILTER SRC_FILES EXCLUDE REGEX ".*impl.cpp")
file(GLOB_RECURSE TEST_FILES RELATIVE /workspace test/*/*_test.cpp)
テスト対象コードとテストコードは add_executable
で追加する. プロジェクト名は google_test
にしてある.
project(google_test CXX)
add_executable(
google_test
${SRC_FILES}
${TEST_FILES}
)
ヘッダファイルはディレクトリを指定する.
target_include_directories(google_test PUBLIC
src/host_communication
src/sensor_communication
test/host_communication
test/sensor_communication
)
それ以外の GoogleTest に必要な設定.
find_package(GTest)
target_link_libraries(
google_test
${GTEST_BOTH_LIBRARIES}
gmock
pthread
GTest::Main
)
target_include_directories(google_test PUBLIC ${GTEST_INCLUDE_DIRS})
テストコードの実行
上述の通りCMakeファイルを読み込んでビルド, テストコードを実行すれば良い.
VScode の拡張機能を使えばIDEと同じように利用できる.
$ code --install-extension \
ms-vscode.cmake-tools \
matepek.vscode-catch2-test-adapter
devcontainer を使う場合は devcontainer.json
に記載することで利用できる(サンプルリポジトリでは導入済み).
テストコードの記述
普通にモックすればいいと思うよ.
テストできない副作用を持つコードをうまく分離することが重要. サンプルリポジトリではシリアル通信部分である HostUart
をモックオブジェクトとしてテスト対象の HostCommunication
にDIする. モックにする HostUart
は抽象クラスとする.
class HostCommunication {
public:
HostCommunication(HostUart* host_uart);
private:
HostUart* host_uart_;
};
class HostUart {
public:
virtual ~HostUart(){};
virtual std::vector<uint8_t> RecvData() = 0;
virtual bool SendString(const std::string s) = 0;
};
モックオブジェクトは Google Mock の通りに作れば良い.
class HostUartMock : public HostUart {
public:
MOCK_METHOD0(RecvData, std::vector<uint8_t>());
MOCK_METHOD1(SendString, bool(const std::string s));
};
実装コードでは抽象クラスを普通に継承して普通に実装する. ヘッダのインクルードが気持ち悪いがこれで実際に利用できる.
#include "../host_communication/host_uart.h"
class HostUartImpl : public HostUart {
public:
HostUartImpl();
HostUartImpl(const uint16_t baudrate);
std::vector<uint8_t> RecvData();
bool SendString(const std::string data);
};
HostUartImpl::HostUartImpl(const uint16_t baudrate) {
Serial.begin(baudrate);
}
HostUartImpl host_uart(HOST_BAUDRATE);
HostCommunication host(&host_uart);
0 件のコメント:
コメントを投稿