[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.

Living in Taiwan. Dancing with C++ and C#. Playing Xamarin. Visual Studio is the most powerful IDE on earth. Learn what I am doing these days at: https://samtsai.org/now/