[JUCE]テスト関係メモ

JUCEでテストを用いて開発を行う際のメモです。
ベストプラクティスというよりはテストのためのHow toのメモになります。

# UnitTest & UnitTestRunner
JUCEに内包されているテスト機能です。
後述のGoogleTestでのテストと比較した際のメリットとしては
・CMakeの設定の必要がなく、比較的にシンプルに記述できる
デメリットとしては
・JUCEのプロダクトコードに直接書く必要があるため、プロダクトとテストが完全に分離されない
UnitTestを継承したクラスを作成し、使用します。
ちなみにJUCEの内部のソースコードを見ると、とある機能のソースコード内にその機能のテストも併せて書いているのが見受けられます。

```c++:MyTest.h
#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);
  }
};
```

```c++:(例)PluginProcessor.cppのコンストラクタ内
// #include "MyTest.h"
UnitTest *_test = new MyTest();  // コンストラクタでテスト配列に追加
UnitTestRunner runner;
runner.runAllTests();
```

```text:テスト結果
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`フォルダを作成し、その中にコードを追加します。

```c++:Add.h
#pragma once
class Add {
public:
    static int calculate(int a, int b);
};
```

```c++:Add.cpp
#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を追加します。

```shell
# cd プロジェクトディレクトリ/Test/third_party
$ git submodule add https://github.com/google/googletest.git
$ cd googletest/
$ git checkout release-1.8.0
```

## テスト用コードの用意
Testフォルダに`src`フォルダを作成し、その中にテスト用コードを用意します。

```c++:MathTest.cpp
#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:CMakeLists.txt
# 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}
)
```

## 最終的なディレクトリ構成
```text:ディレクトリ構成例
project/
  - Builds/
  - JuceLibraryCode/
  - .jucerファイル
  - Source/
    - PluginEditor.h/cpp
    - PluginProcessor.h/cpp
    - Math/
        - Add.h/cpp
  - test/
    - third_party/
    - build
    - CMakeLists.txt
    - src
```

## テストの実行
```shell
# cd プロジェクトディレクトリ/Test/build
$ cmake ..
$ make
$ ./juce-test
```

# Google Mock
前述のGoogle Testのリポジトリで管理されているモックテストのための機能です。
モックテストとは、テスト対象クラスが呼び出している（＝依存している）クラスを正しく利用しているかを検証することです。依存しているクラスをモック(=ダミー)に差し替えてテストします。
以下の例では前項のGoogle Testのプロジェクトに追加変更しています。

## テスト対象コード用意
例として入力信号に対してトレモロ処理を行う`Modulation`クラスとその内部で扱う`Wave`クラスを用意します。
Sourceフォルダに以下にMathフォルダを作成し、その中にコードを追加します。

```c++:Wave.h
#pragma once
class Wave {
public:
    virtual void update(float phase) = 0;
    virtual float get() = 0;
};

```

```c++:Modulation.h
#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;
};

```

```c++:Modulation.cpp
#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フォルダにテスト用コードを用意します。

```c++:MockWave.h
#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());
};

```

```c++:ModulationTest.cpp
#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を追加

```yaml:.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`を選択します。
![Screen Shot 2021-10-01 at 0.53.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/58c155b7-3532-cf3c-3971-6bcc11bcdc5a.png)

pushする度に自動で走っているテストの結果の一覧が表示されます。
![Screen Shot 2021-10-01 at 0.55.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/a97db009-8dfa-ea09-4145-f5d434f753df.png)

passed/failed及びPipeline IDをクリックしてさらに表示される`job`(.gitlab-ci.ymlで指定していたジョブ名)をクリックします。
![Screen Shot 2021-10-01 at 1.00.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/72abd237-c05a-6a50-af9a-0f83e55484c3.png)

詳しいログを確認することができます。
![Screen Shot 2021-10-01 at 1.02.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/35b8d11d-91e2-6ee8-ed92-428caac77c31.png)

## 参考

https://techblog.nhn-techorus.com/archives/12531

https://www.insight-tec.com/tech-blog/20200929_gitlab/

https://github.com/pothitos/gtest-demo-gitlab

# サンプルプロジェクト
https://gitlab.com/migizo/jucetestsample/-/tree/main

# 感想
google testを使う方法はCMakeやGoogle Testの勉強コストがそれなりに掛かってしまうのでプロトタイピング用途としては使わない方が良さそう。
逆に規模が大きくなりそうであったり長期的に保守/改善する必要がありそうな時は状況に併せて上記のいずれかを導入すれば幸せになりそう。