[153] JUCE Diary #14:Catch Test Framework
Catch 在 C++ 單元測試、自動化測試領域算是新兵。由於設計優良,使用簡便,近來頗受好評。JUCE 內建了單元測試機制,雖然大部分情況下比夠用還多,不過,為了避免落入「固步自封」的工程師死亡陷阱,偶爾還是要看看窗外的世界,弄髒手,動動腦。
JUCE 論壇經常出現高手分享自己的作品,昨天就看到 varx 這個把 Reactive-Extension, RxCpp 導入到 JUCE 的專案,太有才,改天一定要試試。不過,今天的重點不在該專案,原因是我挖了專案原始碼來看,發現作者用了 Catch 做單元測試,而不是用 JUCE 內建的機制。然後,使用 Catch 來做測試真的很簡單。底下說明使用方式(程式碼參考 varx 專案)。
首先,使用 Projucer 建一個 Console Application。為什麼要選這個類型的專案?因為「自動產生的程式碼最少」。Main.cpp 長這樣:
#include "../JuceLibraryCode/JuceHeader.h"
int main (int argc, char* argv[])
{
// ..your code goes here!
return 0;
}
接著到 GitHub 下載 Catch,執筆此刻,最新版本為 Catch v1.9.4。下載後將 catch.hpp(對,只有一個檔案,這也是為什麼我會說「使用簡便」)複製到你的專案所在位置,可以放在同一層目錄,但我習慣放在 third_party 子目錄底下,便於管理。
在 Main.cpp 中引入 catch.hpp,並在引入前一行加上 #define CATCH_CONFIG_RUNNER
如下:
#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
其實 Catch 有自己自足的能力,透過 #define CATCH_CONFIG_MAIN
它可以替我產生 main()
,也就是
C++ 程式的進入點(Entry Point),我只要專心寫待測碼即可。不過,由於我要套 JUCE,所以必須自行提供 main
,於是改用 CATCH_CONFIG_RUNNER
。
接下來要建一個 Test Runner,用來執行之後加入的 Test Cases。使用 JUCEApplication
當做 Test Runner,作法是建一個 class 繼承自 JUCEApplication:
class TestRunnerApplication : public JUCEApplication
{
public:
void initialise(const String& command_line) override {}
void shutdown() override {}
const String getApplicationName() override { return ProjectInfo::projectName; }
const String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return false; }
};
START_JUCE_APPLICATION(TestRunnerApplication)
其中 initialise()
and shutdown()
是 JUCE 程式的起始與結束點。START_JUCE_APPLICATION
則是將 TestRunnerApplication
「活化」,讓程式被執行時建立並載入 TestRunnerApplication
個體。由於 JUCE 是跨平台的開發框架,有興趣的人可以深入研究其作法。
最後在 initialise
設定 catch 屬性,並且在跑完測試後自動結束程式:
void initialise(const String& command_line) override
{
Catch::ConfigData config;
config.shouldDebugBreak = true;
Catch::Session session;
session.useConfigData(config);
session.run();
MessageManager::callAsync([] () { quit(); });
}
最後一行使用了 MessageManager
來將呼叫 quit()
延遲到 initialise()
結束之後才做。這麼做是為了避免出現 memory leaks,如果改成直接呼叫 quit()
(原作者是這麼寫),會出現以下錯誤訊息:
Detected memory leaks!
Dumping objects ->
{1894} normal block at 0x00C62B18, 8 bytes long.
Data: <$MV > 24 4D 56 00 01 00 00 00
Object dump complete.
最後的最後,可以開心的寫測試程式碼了:
TEST_CASE("The case of loving someone who don't love you", "[Act I]")
{
auto question = "Do you love me?";
REQUIRE(HerAnswerOf(question) == "Yes!");
}
Catch 還可以用來做 BDD,怎麼做?且待下回分解。
完整程式碼
#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include <JuceHeader.h>
class TestRunnerApplication : public JUCEApplication
{
public:
void initialise(const String& command_line) override
{
Catch::ConfigData config;
config.shouldDebugBreak = true;
Catch::Session session;
session.useConfigData(config);
session.run();
//MessageManager::callAsync([] () { quit(); });
quit();
}
void shutdown() override {}
const String getApplicationName() override { return ProjectInfo::projectName; }
const String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return false; }
};
START_JUCE_APPLICATION(TestRunnerApplication)
String HerAnswerOf(const String& q)
{
return "We can be friends.";
}
TEST_CASE("The case of loving someone who done't love you", "[Act I]")
{
auto question = "Do you love me?";
REQUIRE(HerAnswerOf(question) == "Yes!");
}
Obviously, the test won't pass.