JUCEでテストを用いて開発を行う際のメモです。
ベストプラクティスというよりはテストのためのHow toのメモになります。
UnitTest & UnitTestRunner
JUCEに内包されているテスト機能です。
後述のGoogleTestでのテストと比較した際のメリットとしては
・CMakeの設定の必要がなく、比較的にシンプルに記述できる
デメリットとしては
・JUCEのプロダクトコードに直接書く必要があるため、プロダクトとテストが完全に分離されない
UnitTestを継承したクラスを作成し、使用します。
ちなみにJUCEの内部のソースコードを見ると、とある機能のソースコード内にその機能のテストも併せて書いているのが見受けられます。
#pragma once
#include <JuceHeader.h>
class MyTest : public UnitTest {
public:
MyTest() : UnitTest("juce unit test", "category") {}
// expect()はtrueが期待される式を書きます。
void runTest() override {
Logger::outputDebugString(juce::String("first test"));
beginTest("Part 1");
expect(true);
expect(true);
beginTest("Part 2");
expect(true);
}
};
// #include "MyTest.h"
UnitTest *_test = new MyTest(); // コンストラクタでテスト配列に追加
UnitTestRunner runner;
runner.runAllTests();
JUCE v6.0.7
Random seed: 0x6134947
first test
-----------------------------------------------------------------
Starting test: juce unit test / Part 1...
All tests completed successfully
-----------------------------------------------------------------
Starting test: juce unit test / Part 2...
All tests completed successfully
Google Test
UnitTest & UnitTestRunnerと比較してプロダクトコードに直接テストを書く必要がないためテストとプロダクトコードを分離しやすくなっています。
Google Testの導入方法はいくつかありますが以下はsubmoduleを用いた使用例です。
テスト対象コード用意
理解のしやすさを優先した単純な足し算を行うクラスの例です。
Sourceフォルダに以下にMath
フォルダを作成し、その中にコードを追加します。
#pragma once
class Add {
public:
static int calculate(int a, int b);
};
#include "Add.h"
int Add::calculate(int a, int b) {
return a + b;
}
プロジェクトのgitリポジトリを作成し、submoduleでGoogle Testを導入する
.jucerがあるディレクトリ(プロジェクトディレクトリ)のgitリポジトリをgit initなりで作成します。
自分はGitGUIクライアントであるForkで File > Create New Local Repository...
で作成しました。
プロジェクトディレクトリにTest
フォルダを作成し、さらにその中にthird_party
フォルダを作成します。
third_partyに移動し、submoduleとしてGoogle Testを追加します。
# cd プロジェクトディレクトリ/Test/third_party
$ git submodule add https://github.com/google/googletest.git
$ cd googletest/
$ git checkout release-1.8.0
テスト用コードの用意
Testフォルダにsrc
フォルダを作成し、その中にテスト用コードを用意します。
#include "gtest/gtest.h"
#include "../../Source/Math/Add.h"
TEST(math, add)
{
EXPECT_EQ(2, Add::calculate(1, 1));
EXPECT_EQ(3, Add::calculate(2, 1));
}
CMakeLists.txtの用意
TestフォルダにCMakeLists.txtとbuild
フォルダを作成します。
(※後述のGoogle Mockにも対応したものにしていますがGoogle Mockを使わない場合はビルド時間短縮のため適宜最適化してください)
# cmakeバージョン指定
cmake_minimum_required(VERSION 2.6)
# プロジェクト名を指定
set(PROGRAM juce-test)
project(${PROGRAM})
# 親プロジェクトのコンパイラ・リンカ設定を上書きするのを防ぐ(?)(Windowsのみ)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# Google TestのCMakeプロジェクトを追加
add_subdirectory(third_party/googletest)
# add_executableに指定する依存ソースコードの取得
file(GLOB TEST_SOURCES src/*.cpp)
file(GLOB SOURCES ../Source/Math/*.cpp)
# mainなど不要なファイルを取り除く
# list(REMOVE_ITEM SOURCES src/main.cpp)
# 実行ファイル作成
add_executable(
${PROGRAM}
${SOURCES}
${TEST_SOURCES}
)
# ライブラリを実行ファイルにリンクする
target_link_libraries(${PROGRAM}
PRIVATE
# Google Testをリンクする
gmock
gtest
gmock_main
)
# インクルードディレクトリの追加
target_include_directories(${PROGRAM}
PUBLIC
third_party/googletest/googletest/include
third_party/googletest/googlemock/include
)
# CTestの有効化
enable_testing()
# CTestを利用してテスト作成
add_test(
NAME oogleTestTest
COMMAND ${PROGRAM}
)
最終的なディレクトリ構成
project/
- Builds/
- JuceLibraryCode/
- .jucerファイル
- Source/
- PluginEditor.h/cpp
- PluginProcessor.h/cpp
- Math/
- Add.h/cpp
- test/
- third_party/
- build
- CMakeLists.txt
- src
テストの実行
# cd プロジェクトディレクトリ/Test/build
$ cmake ..
$ make
$ ./juce-test
Google Mock
前述のGoogle Testのリポジトリで管理されているモックテストのための機能です。
モックテストとは、テスト対象クラスが呼び出している(=依存している)クラスを正しく利用しているかを検証することです。依存しているクラスをモック(=ダミー)に差し替えてテストします。
以下の例では前項のGoogle Testのプロジェクトに追加変更しています。
テスト対象コード用意
例として入力信号に対してトレモロ処理を行うModulation
クラスとその内部で扱うWave
クラスを用意します。
Sourceフォルダに以下にMathフォルダを作成し、その中にコードを追加します。
#pragma once
class Wave {
public:
virtual void update(float phase) = 0;
virtual float get() = 0;
};
#pragma once
class Modulation {
public:
Modulation(Wave* wave, float sampleRate = 44100);
void setModulation(Wave* wave);
float process(float inVal, float speed);
private:
Wave* wave;
float counter = 0;
float sampleRate;
};
#include "Modulation.h"
#include <math.h>
Modulation::Modulation(Wave* wave, float sampleRate) {
setModulation(wave);
this->sampleRate = sampleRate;
}
void Modulation::setModulation(Wave* wave) {
this->wave = wave;
}
float Modulation::process(float inVal, float speed) {
wave->update(counter / sampleRate);
counter = fmodf(counter + speed, sampleRate);
return inVal * wave->get();
}
テスト用コードの用意
Test/srcフォルダにテスト用コードを用意します。
#pragma once
#include "gmock/gmock.h"
#include "../../Source/AudioProcess/Wave.h"
class MockWave : public Wave
{
public:
MOCK_METHOD1(update, void(float phase));
MOCK_METHOD0(get, float());
};
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include "../../Source/AudioProcess/Modulation.h"
#include "MockWave.h"
TEST(modulation, mockwaveTest)
{
MockWave wave;
// 期待される関数呼び出しの仕様(=Exception)を用意する
// 今回の場合、update関数が最低でも一回呼ばれるのが期待されている
EXPECT_CALL(wave, update(0)).Times(::testing::AtLeast(1));
Modulation modulation(&wave);
// [PASSED] テストには通るもののget()で返す値が明確に指定されていない旨が言及されます。
modulation.process(0, 0);
// modulation.process(1, 0);
// [FAILED] 2回目でupdate()に渡されている値が0ではないのでテストに失敗する
/*
for (int i = 0; i < 2; i++)
{
modulation.process(0, 1);
}
*/
}
CMakeLists.txtの用意
Google Testの項の
file(GLOB SOURCES ../Source/Math/*.cpp)
を
file(GLOB SOURCES ../Source/*/*.cpp)
に変更します。
テストの実行
Google Testの項と同じです。
CI(GitLab) + Google Test(Mock)
リポジトリをpushする度に自動でテストを走らせることができます。
以下はGitLab CIを用いた例です。
プロジェクトディレクトリに.gitlab-ci.ymlを追加
GitLab CIで行いたい動作を記述した.gitlab-ci.ymlを追加
image: ubuntu:14.04
job:
script:
- apt-get update
- apt-get install -y git cmake g++
- git submodule update --init --recursive
- pushd Test/third_party/googletest
- git checkout release-1.8.0
- popd
- cd Test/build
- cmake ..
- make -j
- ./juce-test
GitLabでリモートリポジトリの作成→pushしてテスト
リモートリポジトリ作成、pushの具体的な手順に関しては省略します。
GitLab上で先ほどのリポジトリを選択し、ロケットのマークのCI > Pipelines
を選択します。
pushする度に自動で走っているテストの結果の一覧が表示されます。
passed/failed及びPipeline IDをクリックしてさらに表示されるjob
(.gitlab-ci.ymlで指定していたジョブ名)をクリックします。
参考
サンプルプロジェクト
感想
google testを使う方法はCMakeやGoogle Testの勉強コストがそれなりに掛かってしまうのでプロトタイピング用途としては使わない方が良さそう。
逆に規模が大きくなりそうであったり長期的に保守/改善する必要がありそうな時は状況に併せて上記のいずれかを導入すれば幸せになりそう。