運用 CppUnit 實作單元測試

   
實習目標 1. 安裝配合 Visual Studio 2010 的 CppUnit

2. 了解類別 Test, TestCase, TestCaller<Fixture>, TestFixture, TestComposite, TestSuite, 與 TestRunner 的運作架構

3. 運用 CPPUnit 的 macros 設計 TestFixture 及 TestSuite
   
步驟一

安裝

請下載 cppunit-1.12.1.VS2010.zip, 這個版本是配合 Visual Studio 2010 Professional 使用的, 解壓縮之後, 到 cppunit-1.12.1\src\ 資料匣裡面點選專案檔 CppUnitLibraries.sln, Visual Studio 2010 會開啟它

  1. 以右鍵選取 cppunit 專案, 選擇「建置」, 會在 cppunit-1.12.1\lib\ 資料匣裡面產生 cppunit.lib

  2. 以右鍵選取 Testrunner 專案, 選擇「建置」, 會在 cppunit-1.12.1\lib\ 資料匣裡面產生 TestRunner.libTestRunner.dll

  3. 以右鍵選取 cppunit_dll 專案, 選擇「建置」, 會在 cppunit-1.12.1\lib\ 資料匣裡面產生 cppunit_dll.libcppunit_dll.dll
下面的動作是在 win7 64bit 系統上做的, 如果你是使用其他的系統, 要找到對應的資料匣
  1. 請將 cppunit-1.12.1\include\ 資料匣中的整個 cppunit 子資料匣拷貝到 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\ 中

  2. 請將 cppunit-1.12.1\lib\ 裡面 cppunit.lib, TestRunner.lib, 還有 cppunit_dll.lib 三個檔案拷貝到 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\ 資料匣中

  3. 請將 cppunit-1.12.1\lib\ 裡面 TestRunner.dll 以及 cppunit_dll.dll 兩個檔案拷貝到 C:\Windows\System32\ 中

CppUnit 在 SourceForge 上的網址為 http://sourceforge.net/projects/cppunit/

CppUnit 的參考資料請見 http://cppunit.sourceforge.net/doc/cvs/index.html, 其中

  1. Modules 的部份是運用 CppUnit 時最主要的說明

  2. Class Hierarchy 的部份是主要的類別架構與個別類別的介面
步驟二

使用的基本概念

  1. 當你需要針對你的應用程式的整體功能, 或是個別類別實現的功能, 撰寫測試程式碼時, 一般不外乎設定一些變數或是物件的初始值, 透過應用程式系統的功能來改變變數的內容或是物件的狀態, 然後比對確認變數內容或是物件狀態是否合乎要求

  2. 每一個測試是一個函式, 測試類似功能的函式可以整合在一起放在同一個類別裡面, 這些測試常常也可以共用環境的初始化程序

  3. 最後把所有的測試表示成一個樹狀的架構, 運用 Interpreter 設計模式來執行所有的測試

CppUnit 裡面我們會使用到的幾個類別組合包括:

  1. Test-TestLeaf-TestComposite 是一組 Composite 設計模式

  2. Test-TestCase-TestCaller<Fixture> 實作每一個類別一個測試的機制

  3. TestFixture 是定義 setUp(), tearDown() 機制的抽象介面

  4. TestSuite 是實際生成樹狀架構的容器類別

整個設計的解釋請參考 BasicDesignOfCppUnit.pdf (4up)

CppUnit 文件

  1. sourceforge 首頁

  2. 類別階層

  3. 用法參考

CppUnit 介紹文章

  1. Cookbook, http://cppunit.sourceforge.net/doc/1.8.0/cppunit_cookbook.html

  2. Money example, http://cppunit.sourceforge.net/doc/cvs/money_example.html

    上面這兩個範例是在 source code 的 doc 裡面提供的

  3. http://www.comp.nus.edu.sg/~cs3214s/tools/cppunitVC.html

  4. http://darkblack01.blogspot.tw/2014/10/cgame-programing-game-6-ch17.html

  5. http://www.ibm.com/developerworks/cn/linux/l-cppunit/

    在 Eclipse/CDT 中使用 CppUnit 請參考

  6. http://pl.csie.ntut.edu.tw/~ctchen/pdf/CPPUnit%20on%20Eclipse.pdf

  7. http://pl.csie.ntut.edu.tw/~ctchen/pdf/JUnitOnEclipse.pdf
步驟三 Visual Studio 專案的屬性設定
  1. '專案 > lab34 屬性頁 > 組態屬性 - C/C++ > 程式碼產生 > 執行階段程式庫 >
    多執行緒偵錯 DLL/MDd'

  2. '專案 > lab34 屬性頁 > 組態屬性 - 連結器 > 輸入'.
    Put 'cppunit.lib' in the '其他相依性' 或運用 #pragma comment(lib,"CppUnit.lib")

  3. '專案 > lab34 屬性頁 > 組態屬性 - 建置事件 > 建置後事件'.
    Put '"$(TargetPath)"' in '命令列'
    Put 'Unit Tests...' in '描述'

命令列編譯

cl /EHsc /MDd /ZI testComplex.cpp Complex.cpp ComplexSingleUnitTest.cpp ComplexMultiUnitTests.cpp cppunit.lib

步驟四

設計單一測試的類別

如果你的專案很單純, 你只需要設計唯一的一個測試, 你可以設計一個類別, 繼承 CppUnit::TestCase 類別, 將測試程式碼寫在 runTest() 成員函式中

----------- ComplexSingleUnitTest.h -----------

#include <cppunit/TestCase.h>

class ComplexSingleUnitTest : public CppUnit::TestCase 
{
public:
    void runTest(); // implement one single test
};


----------- ComplexSingleUnitTest.cpp -----------

void ComplexSingleUnitTest::runTest()
{
    Complex x1,x2,x3,x4;
    x1.setValue(7, 3);
    x2.setValue(2, 5);
    x3.setValue(1, 1);
    x4.setValue(6, -1);

    x1.subtract(x2);
    x1.add(x3);
    CPPUNIT_ASSERT(x1.equal(x4,1e-10));
    CPPUNIT_ASSERT_DOUBLES_EQUAL(x1.magnitude(), x4.magnitude(), 1e-10);
}
CPPUNIT 中多了許多種可以使用的 assertion  巨集
步驟五

設計整合多個測試的類別

設計一個類別, 繼承 CppUnit::TestFixture 類別, 將每一個測試寫成類別的成員函式, 例如下面的 testSetValue(), testAdd() ..., 這些測試所需要的共同環境放在虛擬函式 setUp() / tearDown(), 每一個測試都是獨立的, 在測試的時候, 每一個測試之前會都執行 setUp(), 結束之後都執行 tearDown(), 例如:

setUp();
testSetValue();
tearDown();

setUp();
testAdd();
tearDown();

...

這些測試你可以手動一個一個整合成 TestSuite, 也可以運用 CPPUNIT_TEST_SUITE, CPPUNIT_TEST, CPPUNIT_TEST_SUITE_END 這一組巨集很快地整合成 TestSuite

再運用 CPPUNIT_TEST_SUITE_REGISTRATION() 和
CppUnit::TestFactoryRegistry::getRegistry().makeTest() 就可以迅速得到所有登錄過的 TestSuite

如果你設計了很多個這種 TestFixture 衍生的類別, 你還是可以運用 CPPUNIT_TEST_SUITE_REGISTRATION() 巨集登錄個別的測試, 然後用 makeTest 一次得到所有有登錄的 TestSuite


----------- ComplexMultiUnitTests.h -----------


#include <cppunit/extensions/HelperMacros.h>

class ComplexMultiUnitTests : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE(ComplexMultiUnitTests); CPPUNIT_TEST(testSetValue); CPPUNIT_TEST(testAdd); CPPUNIT_TEST(testSubtract); CPPUNIT_TEST(testMultiply); CPPUNIT_TEST(testDivide); CPPUNIT_TEST_SUITE_END(); public: ComplexMultiUnitTests(void); virtual ~ComplexMultiUnitTests(void); void setUp(); // will be executed before each test void tearDown(); // will be executed after each test void testSetValue(); void testAdd(); void testSubtract(); void testMultiply(); void testDivide(); private: Complex *x1, *x2, *x3; }; ----------- ComplexMultiUnitTests.cpp ----------- #include "ComplexMultiUnitTests.h" #include <cppunit/config/SourcePrefix.h> CPPUNIT_TEST_SUITE_REGISTRATION(ComplexMultiUnitTests); ComplexMultiUnitTests::ComplexMultiUnitTests(void) { } ComplexMultiUnitTests::~ComplexMultiUnitTests(void) { } void ComplexMultiUnitTests::setUp() { x1 = new Complex; x1->setValue(7, 3); x2 = new Complex; x2->setValue(2, 5); x3 = new Complex; x3->setValue(5, -2); } void ComplexMultiUnitTests::tearDown() { delete x1; delete x2; delete x3; } void ComplexMultiUnitTests::testSetValue() { Complex x4; x4.setValue(3, 7); CPPUNIT_ASSERT(!(x1->equal(*x3, 1e-10))); CPPUNIT_ASSERT_DOUBLES_EQUAL(x1->magnitude(), x4.magnitude(), 1e-10); } void ComplexMultiUnitTests::testSubtract() { x1->subtract(*x2); CPPUNIT_ASSERT(x1->equal(*x3,1e-10)); } void ComplexMultiUnitTests::testAdd() { ... } void ComplexMultiUnitTests::testMultiply() { ... } void ComplexMultiUnitTests::testDivide() { CPPUNIT_ASSERT(x1->divide(*x2)); Complex x4; x4.setValue(1,-1); CPPUNIT_ASSERT(x1->equal(x4,1e-10)); x2->setValue(0, 0); CPPUNIT_ASSERT(!x1->divide(*x2)); }
步驟六 在 main 函式中運用 TextUi::TestRunner 文字介面(或是 MfcUi::MfcTestRunner 或是 QtUi::TestRunner 圖形視窗介面)來執行測試並且顯示測試的結果。測試程式碼的執行一般來說和正常的邏輯是不一樣的,通常需要寫獨立的 main() 函式, 當然一個專案裡不能夠有兩個 main() 函式,所以我們會在同一個方案中另外開啟第二個專案來放這些單元測試程式碼
----------- testComplex.cpp -----------

#include <cppunit/CompilerOutputter.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

#include "ComplexSingleUnitTest.h"

int main()
{
    CppUnit::Test *suite1 = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    CppUnit::TestSuite *suite2 = new CppUnit::TestSuite();
    suite2->addTest(new ComplexSingleUnitTest);
    suite2->addTest(suite1);

    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite2);
    bool wasSuccessful = runner.run();
    return wasSuccessful?0:1;
}
請注意用來整合這些測試的 TestSuite 物件 suite2 不可以是堆疊上的區域物件, 需要是 heap 上配置的, 否則結束時會有記憶體刪除的錯誤
步驟七

請完成步驟四中整合多個測試 Complex 類別的單元測試, 助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab3-4 上傳

 

其它範例

1. BubbleSort

2. Student

3. PostfixExpressionInterpreter, pdf, v1, v2

 

步驟五補充說明

// 1. a single TestCase directly run in a runner
int main()
{
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(new ComplexSingleUnitTest("SingleUnitTest"));
    bool wasSuccessful = runner.run();
    return wasSuccessful ? 0 : 1;
}

// 2. two TestCases in a TestSuite
int main()
{
    CppUnit::TestSuite *suite = new CppUnit::TestSuite();
    suite->addTest(new ComplexSingleUnitTest("SingleUnitTest"));
    suite->addTest(new ComplexSingleUnitTest("SingleUnitTest copy 2"));
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite);
    bool wasSuccessful = runner.run();
    return wasSuccessful ? 0 : 1;
}

// 3. a single TestFixture created with helper macro
int main()
{
    CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite);
    bool wasSuccessful = runner.run();
    return wasSuccessful ? 0 : 1;
}

// 4. a single TestFixture with manual construction of TestSuite
int main()
{
    CppUnit::TestSuite *suite1 = new CppUnit::TestSuite();
    suite1->addTest(new CppUnit::TestCaller("sub", &ComplexMultiUnitTest::testSubtract));
    suite1->addTest(new CppUnit::TestCaller("mul", &ComplexMultiUnitTest::testMultiply));
    suite1->addTest(new CppUnit::TestCaller("div", &ComplexMultiUnitTest::testDivide));
    CppUnit::TestSuite *suite2 = new CppUnit::TestSuite("Test 3");
    suite2->addTest(new CppUnit::TestCaller("add", &ComplexMultiUnitTest::testAdd));
    suite2->addTest(suite1);
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite2);
    bool wasSuccessful = runner.run(); 
    return wasSuccessful ? 0 : 1;
}

// 5. combining a TestFixture and a single TestCase with manually constructed TestSuite
int main()
{
    CppUnit::TestSuite *suite1 = new CppUnit::TestSuite();
    suite1->addTest(new CppUnit::TestCaller("sub", &ComplexMultiUnitTest::testSubtract));
    suite1->addTest(new CppUnit::TestCaller("mul", &ComplexMultiUnitTest::testMultiply));
    suite1->addTest(new CppUnit::TestCaller("div", &ComplexMultiUnitTest::testDivide));
    CppUnit::TestSuite *suite2 = new CppUnit::TestSuite("Test 3");
    suite2->addTest(new CppUnit::TestCaller("add", &ComplexMultiUnitTest::testAdd));
    suite2->addTest(new ComplexSingleUnitTest("SingleUnitTest"));
    suite2->addTest(new ComplexSingleUnitTest("SingleUnitTest copy 2"));
    suite2->addTest(suite1);
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite2);
    bool wasSuccessful = runner.run(); 
    return wasSuccessful ? 0 : 1;
}

// 6. combining a TestFixture and two TestCases manually and two TestFixtures with helper macro

---- Complex2ndMultiUnitTests.h ----

class Complex2ndMultiUnitTests : public CppUnit::TestFixture 
{
    CPPUNIT_TEST_SUITE(Complex2ndMultiUnitTests);
    CPPUNIT_TEST(testSetValue);
    CPPUNIT_TEST(testAdd);
    CPPUNIT_TEST(testSubtract);
    CPPUNIT_TEST(testMultiply);
    CPPUNIT_TEST(testDivide);
    CPPUNIT_TEST_SUITE_END();
    ...
};

---- Complex2ndMultiUnitTests.cpp ----

#include "Complex2ndMultiUnitTests.h"
#include <cppunit/config/SourcePrefix.h>

CPPUNIT_TEST_SUITE_REGISTRATION(Complex2ndMultiUnitTests);
...

int main()
{
    CppUnit::Test *suite0 = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    // 5 tests from ComplexMultiUnitTests and 5 tests from Complex2ndMultiUnitTests
    CppUnit::TestSuite *suite1 = new CppUnit::TestSuite();
    suite1->addTest(new CppUnit::TestCaller("sub", &ComplexMultiUnitTests::testSubtract));
    suite1->addTest(new CppUnit::TestCaller("mul", &ComplexMultiUnitTests::testMultiply));
    suite1->addTest(new CppUnit::TestCaller("div", &ComplexMultiUnitTests::testDivide));
    CppUnit::TestSuite *suite2 = new CppUnit::TestSuite("Test 3");
    suite2->addTest(new CppUnit::TestCaller("add", &ComplexMultiUnitTests::testAdd));
    suite2->addTest(new ComplexSingleUnitTest("SingleUnitTest"));
    suite2->addTest(new ComplexSingleUnitTest("SingleUnitTest copy 2"));
    suite2->addTest(suite0);
    suite2->addTest(suite1);
    CppUnit::TextUi::TestRunner runner;
    runner.addTest(suite2); // 16 tests in total
    bool wasSuccessful = runner.run(); 
    return wasSuccessful ? 0 : 1;
}

運用 Google C++ Testing Framework (Google Test) 範例與說明

vc6 可以運作的 CppUnit

 

C++ 物件導向程式設計課程 首頁

製作日期: 03/13/2013 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon