PlatformIOとユニットテスト
今回はPlatformIOでRaspberry Pi Picoのユニットテストを行う場合の設定について考えていきます。特にRaspberry Pi Picoで利用する場合は特殊な設定が必要な箇所があるため、その部分についても合わせて見ていきましょう。またArduinoなどその他のボードでユニットテストを行う場合でも、ほぼ同様のやり方になります。
全体の方針
方針としては、いくつかの関数などよく使うものはネイティブ(ローカル)環境でテスト可能とし、実機が必要な場面では実機でテストするものとします。
そのためネイティブ環境でのテストと、実機でのテストを並行して行えるように環境をセットアップしましょう。つまり、ネイティブとRaspberry Pi Pico用の環境を切り替えられるようにします。
次にユニットテスト用のフレームワークですが、PlatformIOでよく使われるユニットテストフレームワークにUnityがあります。それ以外にもGoogleTestなどが利用可能ですが、対応ボードの多さからUnityを採用します。ちなみに同名のゲームエンジンが存在しますが、全く関係ありません。
またRaspberry Pi PicoでUnityを使ってユニットテストをする場合、いくつかの問題が発生します。これらの対処法についても記載します。
下準備:MSYS2の導入(Windowsの場合)
下準備としてnative環境を利用するためにはGCCが利用できる環境が必要となります。WindowsではMSYS2のインストールを行ってください。
MSYS2導入後は
pacman -S mingw-w64-ucrt-x86_64-gcc
を実行し、GCCの環境を整えます。
また以下のディレクトリ
C:\msys64\mingw64\binC:\msys64\ucrt64\bin
C:\msys64\usr64\bin
を環境変数のPATHに追加しておく必要があります。詳しい情報、またLinux, macOSでの環境設定についてはこちらを御覧ください。
platformio.iniの編集
ではRaspberry Pi Pico用の環境とnative(ローカル)環境の設定を切り替えられるように設定ファイルを記述していきます。
PlatformIOのプロジェクト作成時に生成されるplatformio.iniを次のように編集します。
[env:native]
platform = native
test_framework = unity
test_ignore = test_embedded_pico
[env:pico]
platform = raspberrypi
board = pico
framework = arduino
test_ignore = test_native
test_framework = custom
env:picoがRaspberry Pi Pico用の環境、env:nativeがnative環境です。ポイントはenv:native、env:picoではそれぞれ関係しないディレクトリをtest_ignoreに入れて無視します。またenv:nativeではtest_framework=unityとますが、訳あってenv:picoではtest_framework=customとしています。この理由については後述します。
ファイルとディレクトリ構成
では必要なソースファイルを用意しましょう。
ディレクトリ “lib”
今回は自作のライブラリとして利用するクラスをMyLibrary(.h, .cpp)としました。これらのファイルは”lib/MyLibrary“に入れておきます。
MyLibrary.hとMyLibrary.cppは以下のようになっています。
#ifndef MyLibrary_h
#define MyLibrary_h
class MyLibrary{
public:
static int testFunction();
};
#endif
#include "MyLibrary.h"
int MyLibrary::testFunction(){
return 5;
}
これはテスト用の関数testFunction()だけを備えたシンプルなもので、testFunctionはただ整数値の”5″を返すだけのものです。
ディレクトリ “src“
またプロジェクトを作成した際にディレクトリ”src“にmain.cppが生成されているかと思います。今回このファイルは利用しませんが、本来開発ではここにプログラムを記述していくと思いますので自動生成されたものをそのままにしておきます。
ディレクトリ “test”
ユニットテストのテストコードはディレクトリ”test“の下に配置します。このディレクトリ以下には、native用のテストコードを配置するディレクトリ”test_native“とpico用のテストコードを配置するディレクトリ”test_embedded_pico“を用意し、そこにコードを準備します。
native用テストコードをtest_native.cpp、 pico用のテストコードをtest_embedded_pico.cppとします。
ディレクトリ “src”と”lib”の役割
テスト実行時にはディレクトリ”src“内のソースコードは特別な設定をしない限りビルドされません。実際のプログラム、テストコード両方で使うソースコードはディレクトリ”lib“の下に配置するようにしましょう。
(今回は利用しませんが、ディレクトリ”src“以下のコードもテスト時にビルドするオプションはtest_build_srcというものが用意されているようです。)
その他 必要なファイル
さらに、picoでのテスト環境用にtest_custom_runner.pyというファイルがディレクトリ”test“の直下に必要となります。
ディレクトリ構造は図1のようになります。
カスタムテストフレームワークの導入
さてここで、最後に導入した”test_custom_runner.py“はどのようなものか見ていきましょう。これは環境env:picoで設定した”test_framework=custom“に対応するもので、ユーザーが定義したテスト環境を実行するものです。
なぜこのようなものを用意してカスタムしたテストを実行するのか、これはRaspberry Pi PicoでUnityのテストをする際に発生する問題を回避するためです。
というのも、PlatformIOでRaspberry Pi Picoの開発を行う場合、先程のplatformio.iniにおいて”framework=arduino“としていますが、このときArduino Mbed OS RP2040 Boardsがファームウェアとして利用されます。(earlephilhower版を使っている場合はその限りではありません。)
実はこのファームウェア自体にUnityが含まれているため、改めてUnityのライブラリをリンクしようとすると多重になってしまいエラーが出ます。(参考)
よってこの状態で、”test_framework=unity“としてしまうと、ビルド時に
collect2: error: ld returned 1 exit status
となってエラーで終了となってしまいます。
こうした問題は、Arduino Nano 33 BLEなどmbed coreを利用しているArduinoボードでも同様だと考えられます。
そこでこうした現象を回避するためのカスタムテストを用意する必要があるのです。とはいえ、その実装は実にシンプルなものでOKなので、準備していきましょう。
test_custom_runner.pyを次のように実装します。
from platformio.public import UnityTestRunner
class CustomTestRunner(UnityTestRunner):
EXTRA_LIB_DEPS = None
def configure_build_env(self, env):
pass
このコードはこちらの情報を参考にしています。またカスタムテストについての詳細はこちらです。
ここまで用意できれば準備完了です。それでは実際にテストコードを実装していきましょう。
テストコードの作成
test_native.cppとtest_embedded_pico.cppをそれぞれ以下のように実装します。
test_native.cpp
#include <unity.h>
#include "MyLibrary.h"
void setUp(void){
}
void tearDown(void){
}
void testMyLibrary(){
TEST_ASSERT_EQUAL_INT(5, MyLibrary::testFunction());
}
int runUnityTests(void){
UNITY_BEGIN();
RUN_TEST(testMyLibrary);
return UNITY_END();
}
int main(void){
return runUnityTests();
}
test_embedded_pico.cpp
#include <Arduino.h>
#include <unity.h>
#include "MyLibrary.h"
REDIRECT_STDOUT_TO(Serial);
void setUp(void){
}
void tearDown(void){
}
void testMyLibrary(){
TEST_ASSERT_EQUAL_INT(5, MyLibrary::testFunction());
}
int runUnityTests(void){
UNITY_BEGIN();
RUN_TEST(testMyLibrary);
return UNITY_END();
}
void setup(){
while(!Serial){}
runUnityTests();
}
void loop(){
}
このテストではテスト用関数testMyLibrary()を実行し、テストを行います。TEST_ASSERT_EQUAL_INTではMyLibraryのtestFunction()の返す値が整数値の”5“であることを確認しています。先程実装したtestFunction()は整数の5を返すだけのメソッドとして実装していますので、このテストは問題なく通るはずです。
またUnityの仕様として、setUp()とtearDown()を実装します。setUp()はテスト実施前に実行される関数、tearDown()はテスト実施後に実行される関数です。このsetUp()はtest_embedded_pico.cppのsetup()とは異なります。(setUp()とsetup()なのでプログラム的には区別されている。)
test_native.cppの実装について
まずtest_native.cppの方ですが、こちらではローカル実行の一般的なC/C++プログラムですので、int main()を用意します。実行するとこの関数から実行が始まります。
test_embedded_pico.cppの実装について
一方でtest_embedded_pico.cppではmain()関数を用意せず、代わりに通常のArduinoスケッチのようにsetup()とloop()関数を用意します。
ここで、Raspberry Pi Picoで利用する際のもう一つのポイントがあります。
REDIRECT_STDOUT_TO(Serial);
この行を追加しないと、テスト結果が正しく表示されません。また、setup()ではシリアル通信が準備完了するまで待機するように
while(!Serial){}
を追加しています。
その他のテスト用関数など、Unityの詳しい利用方法についてのリファレンスはこちらです。
テストの実施
それではテストを行っていきましょう。
まずはtest_nativeから行います。開発環境がenv:nativeになっていない場合は図2の用に切り替えます。
環境をenv:nativeにしたら、テスト実行ボタンを押します。(図3)
実行すると、図4のように表示されるはずです。
無事、test_nativeが”PASSED“になっており、テストが成功しています。
次にtest_embedded_picoを実行する時は環境をenv:picoに切り替え、Raspberry Pi Picoを接続します。その状況で、同じくテスト実行ボタンを押すと図5のようになるはずです。
こちらもtest_embedded_picoが”PASSED“となり成功しています。
うまく実行できないときは
設定のミスなどでUnityのライブラリがすでにインストールされてしまっている場合があります。その際は”.pio/libdeps“を一旦削除し、再構築するとうまくいくかもしれません。(参考)
Githubでサンプル公開中!
今回用いたサンプルはGithubで公開していますので、合わせてご覧下さい。
またArduinoなど他のボードを利用する場合でも基本的な流れは同様です。ただし、ボードによって今回のRaspberry Pi Picoのような専用の設定をする必要があるかもしれません。
最新情報はこちら!
最新の開発状況は以下で公開中です!ぜひご覧ください!