cppunit 單元測試

本文是一篇簡單的入門指導,幫助你快速上手。

簡單測試用例(Simple Test Case)

你希望知道你的代碼是否正在工作。
你該怎麼辦?
有很多種方法。使用調試器直接調試或者在你的代碼裡亂丟一些流輸出指令是兩種簡單的方法,但是它們都有自己的缺點。直接調試代碼是個好主意,但是它不是自動進行的。你不得不在每次改動代碼以後重新調試。輸出流文本也不錯,但是它使代碼變得面目可憎,並且大多數情況下,它輸出的信息比你想要的要多。

在CppUnit中測試可以自動進行。這些測試可以很容易被建立,並且一旦你書寫完畢,他們可以幫助你時刻了解你代碼的質量。

為了做一個簡單的測試,下面這些是你要做的:

從TestClass中派生一個類。Override runTest()方法。當你希望檢查一個值的時候,調用 CPPUNIT_ASSERT(bool),如果測試成功這個assert表達式可以被順利通過。

比如,為了測試一個複數類的等值比較,書寫如下:

class ComplexNumberTest : public CppUnit::TestCase {
public:
  ComplexNumberTest( std::string name ) :    CppUnit::TestCase( name ) {}
 
  void runTest() {
    CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
    CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
  }
};

這就是一個簡單的測試。通常來說,你會有很多小的測試用例,並且希望能在同一個對象集合中測試。為了達到這個目的,使用fixture。

Fixture
一個fixture是一組對象,被用來作為測試一組用例的基礎。當你邊開發邊測試的時候,使用fixture會非常方便。

那我們嘗試一下這種開發方式,同時學習一下fixture的使用方法。假定我們就是想開發一個複數的類,我們從定義一個名為Complex的空類開始。

class Complex{};

現在建立上面那個ComplexNumberTest測試用例,編譯它們看看會發生什麼。我們注意到的第一件事是有一些編譯錯誤。測試使用了操作符==,但是它並沒有被定義。修改如下:

bool operator==( const Complex & a, const Complex & b )
{
 return true;
}

現在再次編譯並運行之。這次編譯通過了,但是沒有通過測試。我們需要再寫些代碼使操作符==可以正確工作,所以我們再次修改代碼:

class Complex {
  friend bool operator ==(const Complex& a, const Complex& b);
  double real, imaginary;
public:
  Complex( double r, double i = 0 )
    : real(r)
        , imaginary(i)
  {
  }
};

bool operator ==( const Complex &a, const Complex &b )
{
  return eq( a.real, b.real )  &&  eq( a.imaginary, b.imaginary );
}

如果我們現在編譯並運行,就可以順利通過測試。

現在我們準備添加一些新的操作符和新的測試用例。這時使用一個fixture會很方便。如果我們實例化3到4個複數並在測試中反復使用它們,可能我們的測試會更好些。
我們這樣做:
* 為fixture的每個部分添加成員變量。
* Override setUp() 初始化這些變量。
* Override tearDown()釋放你在setUp()中使用的資源。

class ComplexNumberTest : public CppUnit::TestFixture {
private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }
};

一旦我們擁有了這個fixture,我們就可以添加操作符+,以及整個開發過程中其他的任何操作符。

Test Case
為了使用一個fixture來調用單獨的測試,該如何做呢?
分為兩個步驟:
*以一個method的形式,在fixture類中寫一個測試用例
*創建TestCaller來運行那個method

這裡是我們加了一些額外的用例method書寫的測試類:
private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};

我們可以象下面這樣為每個測試用例創建並運行一個實例:
CppUnit::TestCaller<ComplexNumberTest> test( "testEquality",
                                             &ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );

TestCaller的構造函數的第二個參數是ComplexNumberTest中對應method的地址。當這個TestCaller運行的時候,指定的method會運行。但是,這個辦法也效果不彰,因為它不顯示診斷信息。我們可以使用TestRunner(下面會講到)來顯示這個診斷信息。

一旦我們有了幾個測試用例,可以把它們歸入一個suite中。

Suite
為了建立多個用例並且讓它們一次全部運行,你該如何做呢?
CppUnit提供了一個TestSuite類來同時運行任意個用例。
在上面我們看到了如何運行一個測試用例。
為了創建含有兩個或更多用例的suite,你應該這麼辦:
CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                       "testEquality",
                       &ComplexNumberTest::testEquality ) );
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                       "testAddition",
                       &ComplexNumberTest::testAddition ) );
suite.run( &result );

TestSuites不必僅僅含有測試用例的caller.它們可以包含實現Test 接口的任意對象。例如:你可以在你的代碼中創建一個TestSuite,我也可以在我的代碼裡建立一個,我們通過建立一個同時包含它們兩個的TestSuite使它們得以同時運行。
CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( ComplexNumberTest::suite() );
suite.addTest( SurrealNumberTest::suite() );
suite.run( &result );

TestRunner
你該如何運行你的用例並收集測試結果呢?

一旦你有了一個TestSuite, 你會想運行它。CppUnit提供了定義這些suite並顯示結果的工具。你可以通過在一個TestSuite中加入一個名為suite的靜態的method使你的suite與TestRunner建立聯系。

例如,為了使TestRunner可以看到一個ComplexNumberTest suite,在ComplexNumberTest中加入一下代碼:
public:
  static CppUnit::Test *suite()
  {
    CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testEquality",
                                   &ComplexNumberTest::testEquality ) );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testAddition",
                                   &ComplexNumberTest::testAddition ) );
    return suiteOfTests;
  }

為了使用這個版本,在Main.cpp中包含以下頭文件:
#include <cppunit/ui/text/TestRunner.h>
#include "ExampleTestCase.h"
#include "ComplexNumberTest.h"

然後在main()中加入addTest(CppUnit::Test *)的調用:
int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  runner.addTest( ExampleTestCase::suite() );
  runner.addTest( ComplexNumberTest::suite() );
  runner.run();
  return 0;
}

TestRunner會運行這些用例。如果所有的測試都順利通過,你會得到一個反饋。如果任何一個測試沒有通過,你會得到以下信息:
*失敗的測試用例的名字
*包含這個測試源文件的名字
*錯誤發生的行號
*發現錯誤的CPPUNIT_ASSERT()調用中的所有文字。

Helper Macros
你可能已經注意到了,實現fixture中的static suite()是一個要反復要做的,並且容易出錯的任務。我們可以使用一組寫 test fixture的巨集來自動執行這些靜態的suite method.

下面是使用這些巨集重寫了類ComplexNumberTest後的代碼:
#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  {

首先我們聲明這個suite,把這個類的名字指定給巨集:
CPPUNIT_TEST_SUITE( ComplexNumberTest );

這個使用靜態的suite() method建立的suite以類的名字來命名。然後,我們為每個測試用例聲明:
CPPUNIT_TEST( testEquality );
CPPUNIT_TEST( testAddition );

最後,我們結束這個suite聲明:
CPPUNIT_TEST_SUITE_END();

在這裡下面這個method已經被實現了:
static CppUnit::TestSuite *suite();

剩下的fixture保持不動:
private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};

加入這個suite的TestCaller的名字是這個fixture名字和method名字的組合。
對目前這個用例來說,名字會是"ComplexNumberTest.testEquality" 和"ComplexNumberTest.testAddition".

helper macros幫你寫些常用的斷言。例如。檢查當一個數被零除的時候ComplexNumber是否會拋出MathException異常:
*把這個測試用例加入使用CPPUNIT_TEST_EXCEPTION的suite中,指定希望的異常的類型。
*寫這個測試用例的method

CPPUNIT_TEST_SUITE( ComplexNumberTest );
// [...]
CPPUNIT_TEST_EXCEPTION( testDivideByZeroThrows, MathException );
CPPUNIT_TEST_SUITE_END();

// [...]

  void testDivideByZeroThrows()
  {
    // The following line should throw a MathException.
    *m_10_1 / ComplexNumber(0);
  }

如果期望的異常沒有被拋出,這個斷言就會失敗。

TestFactoryRegistry

TestFactoryRegistry是用來解決以下兩個缺陷的:
*忘了把你的fixture suite加入test runner(因為它在另外一個文件中,很容易忘)
*因為加入所有測試用例頭文件造成的編譯瓶頸。

TestFactoryRegistry是在初始化的時候注冊suite的地方。

為了注冊ComplexNumber suite,在.cpp中加入:
#include <cppunit/extensions/HelperMacros.h>

CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumber );

事實上,桌面下的動作是,一個靜態的AutoRegisterSuite類型變量被聲明。在構建的時候,它會注冊一個TestSuiteFactory到TestFactoryRegistry。 TestSuiteFactory返回ComplexNumber::suite()返回的TestSuite。

為了運行這些用例,使用文字的test runner,我們不必包含fixture了:
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;

首先我們得到TestFactoryRegistry的實例:
 CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();
然後我們獲得並添加一個由TestFactoryRegistry產生的新的TestSuite,它包含了使用CPPUNIT_TEST_SUITE_REGISTRATION()注冊的所有的test suite.
  runner.addTest( registry.makeTest() );
  runner.run();
  return 0;
}

Post-build check
好了,現在我們已經可以使測試運行了,那麼把它集成到編譯過程中去怎麼樣?
為了達到這個目的,應用程序必須返回一個非0值表明出現了錯誤。
TestRunner::run()返回一個布爾值來表明run()是否成功。
更新一下我們的main函數,我們得到:
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry &registry =       CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest( registry.makeTest() );
  bool wasSucessful = runner.run( "", false );
  return wasSucessful;
}

現在,你需要編譯後運行你的應用程序。
使用 Visual C++的話,可以在Project Settings/Post-Build step中加入下面的命令。它被擴展到應用程序的執行路徑。使用這個技術的時候看看project examples/cppunittest/CppUnitTestMain.dsp 中是如何設置的。

Original version by Michael Feathers. Doxygen conversion and update by Baptiste Lepilleur

arrow
arrow
    全站熱搜

    Bluelove1968 發表在 痞客邦 留言(0) 人氣()