toms.log
2022/05/06
Raspberry Pi Pico + Arduino + GoogleTest
ファームウェアの単体テスト
Arduino 開発環境で真面目に単体テストするためにGoogleTestを導入する. サンプルリポジトリは以下.
[toms74209200/arduino_gtest_sample: C++ GoogleTest sample for Arduino farmware](https://github.com/toms74209200/arduino_gtest_sample) ## 環境 何でもかんでも 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](https://github.com/toms74209200/arduino_gtest_sample/blob/master/.devcontainer/Dockerfile) に書いてある. ```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 を使用する. これを使うことで 1. `CMakeList.txt` 読み込み 2. ビルド 3. テスト実行 の3ステップでテストできるようになる. CMake なんもわからんが, テストコードのビルドのためにしか使わないのでそこまで考えずにテスト用にビルドプロジェクトを作れば良い. CMake でファームウェアのビルドするようなところはもうちょっとちゃんとした秘伝のタレがあるはず. テスト対象の実装コードとテストコードは `file` コマンドで探索する. このときテストから除外するコードは `EXCLUDE` で除外する. ```cmake 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` にしてある. ```cmake project(google_test CXX) add_executable( google_test ${SRC_FILES} ${TEST_FILES} ) ``` ヘッダファイルはディレクトリを指定する. ```cmake target_include_directories(google_test PUBLIC src/host_communication src/sensor_communication test/host_communication test/sensor_communication ) ``` それ以外の GoogleTest に必要な設定. ```cmake 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と同じように利用できる. ```bash $ code --install-extension \ ms-vscode.cmake-tools \ matepek.vscode-catch2-test-adapter ``` devcontainer を使う場合は [`devcontainer.json`](https://github.com/toms74209200/arduino_gtest_sample/blob/master/.devcontainer/devcontainer.json#L12) に記載することで利用できる(サンプルリポジトリでは導入済み). ### テストコードの記述 普通にモックすればいいと思うよ. テストできない副作用を持つコードをうまく分離することが重要. サンプルリポジトリではシリアル通信部分である `HostUart` をモックオブジェクトとしてテスト対象の `HostCommunication` にDIする. モックにする `HostUart` は抽象クラスとする. ```c++ class HostCommunication { public: HostCommunication(HostUart* host_uart); private: HostUart* host_uart_; }; ``` ```c++ class HostUart { public: virtual ~HostUart(){}; virtual std::vector
RecvData() = 0; virtual bool SendString(const std::string s) = 0; }; ``` モックオブジェクトは Google Mock の通りに作れば良い. ```c++ class HostUartMock : public HostUart { public: MOCK_METHOD0(RecvData, std::vector
()); MOCK_METHOD1(SendString, bool(const std::string s)); }; ``` 実装コードでは抽象クラスを普通に継承して普通に実装する. ヘッダのインクルードが気持ち悪いがこれで実際に利用できる. ```c++ #include "../host_communication/host_uart.h" class HostUartImpl : public HostUart { public: HostUartImpl(); HostUartImpl(const uint16_t baudrate); std::vector
RecvData(); bool SendString(const std::string data); }; HostUartImpl::HostUartImpl(const uint16_t baudrate) { Serial.begin(baudrate); } ``` ```c++ HostUartImpl host_uart(HOST_BAUDRATE); HostCommunication host(&host_uart); ``` ## Refs. - [arduino/arduino-cli: Arduino command line tool](https://github.com/arduino/arduino-cli) - [googletest/README.md at main · google/googletest](https://github.com/google/googletest/blob/main/googletest/README.md) - [超入門編 — Google Mock ドキュメント日本語訳](http://opencv.jp/googlemockdocs/fordummies.html)
0 件のコメント:
コメントを投稿
次の投稿
前の投稿
ホーム
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿