はじめに
この記事は、『テスト駆動開発による組み込みプログラミング 』に触発された筆者が、ESP32のアプリをテスト駆動開発するために試行錯誤した成果の記録です。
次の2点を通して ESP32・ホストの両方でテストを実行できるプロジェクトの作成方法を紹介します。
- ESP-IDF と CppUTest を用いたプロジェクトの作成方法
- ホスト環境等で、ESP-IDFに依存する実装をテストダブルに置き換えてビルドする方法
おしながき
- CppUTest の導入
- ESP-IDF の導入
- ESP-IDF/テストダブルの切り替え
検証環境
記事の内容は、下記の環境をでビルド・テスト実行できることを確認したものです。
- ESP32-Devkit-C
- Ubuntu 20.02 on WSL2 (Windows 11)
- CMake
- CppUTest
- ESP-IDF
(注意) WSL で ESP32 と接続するにはひと工夫必要です。-> 別記事参照
CppUTest の導入
CppUTest は C/C++ 向けのテストライブラリの一つです。
ホスト上で実行するだけであれば、CppUTest のプリビルドを利用できるのですが、
アーキテクチャが違うので ESP32 上では実行できません。
なので、cpputest のソースをダウンロードして、プロジェクトのビルド時に cpputest も含めてビルドするようにします。
まず、CppUTest は GitHub で公開されているので、それをプロジェクトに追加します。
以下では、Git でバージョン管理しているプロジェクトに submodule として取り込む例です。
~/MyPrj$ git submodule add https://github.com/cpputest/cpputest.git cpputest
次に、ダウンロードしたソースをビルド対象に指定します。
以下の例では、 CMake でビルドするために、CMakeLists.txt
を作成しました。
テストに必要な実装は以下3種です。
-
src/CppUTest
内のソースファイル -
include
内のヘッダーファイル -
src/Platforms/
コンパイラに依存する部分のソースファイル
# ソースファイルを追加する
set(CPPUTEST_HOME ${PROJECT_SOURCE_DIR}/cpputest)
add_subdirectory(${CPPUTEST_HOME}/src/CppUTest)
# 実行ファイルにコンパイラの依存を指定する
set(elf_file ${CMAKE_PROJECT_NAME}.elf)
add_executable(${elf_file}
tests/AllTests.cpp
${CPPUTEST_HOME}/src/Platforms/Gcc/UtestPlatform.cpp # g++ でコンパイルするためにGccを指定
${SRC_FILES}
${TEST_FILES})
# 実行ファイルにインクルードする
target_include_directories(${elf_file}
PUBLIC ${CPPUTEST_HOME}/include
PUBLIC ${HEADER_DIRS})
この時点で、ホスト環境でのテストができるようになっているはずです。
$ cmake ..
$ cmake --build .
$ ./MyPrj.elf
Linking YourProject_tests
Running YourProject_tests
........
OK (8 tests, 8 ran, 35 checks, 0 ignored, 0 filtered out, 0 ms)
ESP-IDF の導入
idf_as_lib の方法を用いて、ホスト環境で実行していたテストを ESP32上で実行できるようにします。
これを用いることで、既存のプロジェクトを簡単に ESP-IDF に取り込むことができます。
ESP-IDFとプロジェクトとの依存を切りやすいのでおすすめです。
詳細は、公式のAPI Guides と Example を参照してください。
必要な対応は2点です。
- CMakeLists.txt に ESP-IDF 対応の項目を記載する
- main 関数が ESP-IDFから読み込まれるようにする
1 は次節でまとめて説明するので、ここでは 2 について説明します。
API Guides によると、ESP32でアプリ起動されるときは、まず app_main
関数が実行されます。
そこで、main 関数を実行する app_main
関数を追加します。
(注意) main を C++ で書いている場合には、 extern "C"
を忘れないでください! app_main
関数を実行する ESP-IDF側の実装 がC言語で書かれているので、C++ を実行するために必要です。
#include <CppUTest/CommandLineTestRunner.h>
extern "C" { // <- ESP-IDF側の呼び出し元が C言語で書かれているために必要
namespace Switch {
int main(int ac, char** av) {
return CommandLineTestRunner::RunAllTests(ac, av);
}
void app_main() { // ESP-IDF 用の関数
char* argv[] = {""};
main(1, argv);
}
} // namespace Switch
}
ESP-IDF/テストダブルの切り替え
ホスト上でテストする場合には、ESP-IDFは利用できないので、テストダブル(モック・スタブ)を使ってテストします。
「テスト駆動開発による組み込みプログラミング 」によると、プラットフォームに依存する実装とそのテストダブルの切り替えはリンカで行うのが良いそうです。
下記の CMakeLists.txt では、CMake 実行時の引数の値によって ESP-IDF を利用する部分の ヘッダとソースを切り替えています。
また、リンクするライブラリは、 ESP32 用のビルドでのみ ESP-IDF を追加するようにしています。
...(前略)...
# ターゲットによってインクルード元を変える
if("${TARGET}" STREQUAL "esp32")
include($ENV{IDF_PATH}/tools/cmake/idf.cmake)
idf_build_process(esp32
SDKCONFIG ${CMAKE_BINARY_DIR}/sdkconfig
BUILD_DIR ${CMAKE_BINARY_DIR})
set(HAL_DIR_NAME esp32)
set(HEADER_DIRS_HAL ${HEADER_DIRS_HAL}
$ENV{IDF_PATH}/components/esp_common/include)
else()
set(HAL_DIR_NAME mocks)
endif()
...(略)...
# ターゲットによってリンク元を変える
if("${TARGET}" STREQUAL "esp32")
target_link_libraries(${elf_file}
idf::esp32
idf::freertos
idf::spi_flash
CppUTest
Hal)
idf_build_executable(${elf_file})
else()
target_link_libraries(${elf_file} PUBLIC CppUTest Hal)
endif()
上記の CMakeLists を用いてビルドするとき、CMake の引数を変えることによって IDF/ホストを切り替えます。
- -DCMAKE_TOOLCHAIN_FILE: コンパイラの指定。ESP-IDFように指定が必要 (デフォルトは g++)。
- -DTARGET: ターゲットデバイスの指定。esp32 が指定されたときのみ、ESP-IDF を用いた実装にする。
$ cd build
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=$IDF_PATH/tools/cmake/toolchain-esp32.cmake -DTARGET=esp32 -GNinja && cmake --build .
$ python $IDF_PATH/components/esptool_py/esptool/esptool.py -p /dev/{ESP32のポート} write_flash @flash_project_args
$ python $IDF_PATH/tools/idf_monitor.py -p /dev/{ESP32のポート} MyPrj.elf
$ cmake ..
$ cmake --build .
$ ./MyPrj.elf