[ESP-IDF][C++]  ESP32でテスト駆動開発するための下準備

## はじめに

この記事は、『[テスト駆動開発による組み込みプログラミング](https://www.oreilly.co.jp/books/9784873116143/) 』に触発された筆者が、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 と接続するにはひと工夫必要です。-> [別記事参照](https://qiita.com/isseikz/items/344a20570e23310cac8d)


## CppUTest の導入

CppUTest は C/C++ 向けのテストライブラリの一つです。

ホスト上で実行するだけであれば、CppUTest のプリビルドを利用できるのですが、
アーキテクチャが違うので ESP32 上では実行できません。

なので、cpputest のソースをダウンロードして、プロジェクトのビルド時に cpputest も含めてビルドするようにします。


まず、CppUTest は GitHub で公開されているので、それをプロジェクトに追加します。
以下では、Git でバージョン管理しているプロジェクトに submodule として取り込む例です。

```bash:CppUTestをプロジェクトに追加する
~/MyPrj$ git submodule add https://github.com/cpputest/cpputest.git cpputest
```


次に、ダウンロードしたソースをビルド対象に指定します。
以下の例では、 CMake でビルドするために、`CMakeLists.txt` を作成しました。

テストに必要な実装は以下3種です。

* `src/CppUTest`内のソースファイル
* `include`内のヘッダーファイル
* `src/Platforms/`コンパイラに依存する部分のソースファイル

```cmake:MyPrj/CMakeLists.txt(抜粋)
# ソースファイルを追加する
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})
```


この時点で、ホスト環境でのテストができるようになっているはずです。

```bash:ホスト環境でのテスト
$ 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](https://github.com/espressif/esp-idf/tree/release/v4.3/examples/build_system/cmake/idf_as_lib) の方法を用いて、ホスト環境で実行していたテストを ESP32上で実行できるようにします。
これを用いることで、既存のプロジェクトを簡単に ESP-IDF に取り込むことができます。
ESP-IDFとプロジェクトとの依存を切りやすいのでおすすめです。

詳細は、公式の[API Guides](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32/api-guides/build-system.html#using-esp-idf-in-custom-cmake-projects) と [Example](https://github.com/espressif/esp-idf/tree/release/v4.3/examples/build_system/cmake/idf_as_lib) を参照してください。

必要な対応は2点です。

1. CMakeLists.txt に ESP-IDF 対応の項目を記載する
1. main 関数が ESP-IDFから読み込まれるようにする

1 は次節でまとめて説明するので、ここでは 2 について説明します。

[API Guides](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/startup.html#application-startup) によると、ESP32でアプリ起動されるときは、まず `app_main` 関数が実行されます。
そこで、main 関数を実行する `app_main` 関数を追加します。

(注意) main を C++ で書いている場合には、 `extern "C"` を忘れないでください！ `app_main`関数を実行する [ESP-IDF側の実装](https://github.com/pycom/esp-idf-2.0/blob/092aa8176ffa0ab386fb6d33e50e1a267bef9d1c/components/esp32/cpu_start.c#L267) がC言語で書かれているので、C++ を実行するために必要です。



```cpp:tests/RunAllTests.cpp
#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は利用できないので、テストダブル(モック・スタブ)を使ってテストします。

「[テスト駆動開発による組み込みプログラミング](https://www.oreilly.co.jp/books/9784873116143/) 」によると、プラットフォームに依存する実装とそのテストダブルの切り替えはリンカで行うのが良いそうです。

下記の CMakeLists.txt では、CMake 実行時の引数の値によって ESP-IDF を利用する部分の ヘッダとソースを切り替えています。
また、リンクするライブラリは、 ESP32 用のビルドでのみ ESP-IDF を追加するようにしています。

```cmake:CMakeLists.txt

...(前略)...

# ターゲットによってインクルード元を変える
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 を用いた実装にする。

```bash: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
```

```bash:テストダブルを使ってビルドする
$ cmake ..
$ cmake --build .
$ ./MyPrj.elf
```