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.