2014年10月28日 星期二

C++單元測試(7) - Game Programing Game 6 Ch1.7 貳部曲

貳部曲!!這次準備要介紹:
  1. 如何測試函數的正確性
  2. 如何測試拋出正確的例外處理
假設,我們即將設計一個像這樣的類別
//model.h
typedef int model_type;
typedef int vertex_t;
typedef int tringle_t;
typedef int mesh_t;
typedef int material_t;
typedef int joint_t;


class model
{
public:
void loadfile(const char* file_path);
void render();
void animate(float speed, bool loop = true);

//我們利用一些實用函數來加載模型文件
void parse_header_section(char* file_text);
void parse_triangle_section(char* file_text);
void parse_mesh_section(char* file_text);
void parse_material_section(char* file_text);
void parse_animation_section(char* file_text);
void preare_joints();
//從文件中加載紋理(支援bmp檔)
void load_texture(const char* file_path);
//讀取檔案資訊的函數
double get_version() const
{ return m_version; }
model_type get_type() const
{ return m_model_type; }
const char* get_name() const
{ return m_name; }
const char* get_author() const
{ return m_author; }
//讀取資料函數
size_t get_number_of_vertices() const
{ return m_number_of_vertices; }
size_t get_number_of_triangles() const
{ return m_number_of_triangles; }
size_t get_number_of_meshes() const
{ return m_number_of_meshes; }
size_t get_number_of_materials() const
{ return m_number_of_materials; }
size_t get_number_of_joints() const
{ return m_number_of_joints; }
private:
double m_version;
model_type m_model_type;
char* m_name;
char* m_author;
unsigned short m_number_of_vertices;
unsigned short m_number_of_triangles;
unsigned short m_number_of_meshes;
unsigned short m_number_of_materials;
unsigned short m_number_of_joints;

vertex_t* m_vertices;
tringle_t* m_triangles;
mesh_t* m_meshes;
material_t* m_materials;
joint_t* m_joints;
};

以下,將對一個parse_header_section()做單元測試。
它是一個public的函數,也是一般我們會做單元測試的範圍。
因為「在不更動public函數介面的情況之下,讓private的可讀性提昇」的情況之下重構,就可以在不變更unit test function的情況之下,知道你的程式碼有沒有改壞了。(所以,設計一個有好public function的class,是首要之事呀)

單元測試的內容,包含了正常的測試也包含了非正常情況的測試。
在此,我們都會對parse_header_section()做測試,不過兩種測試都是針對「可預期情況」做測試。

非正常條件的測試
/* 第一個測試例子是一個比較簡單的測試:
向這個函數傳遞一個非法的參數,我們就假設去傳入一個空指標。
我們預期的是,如果傳遞給函數的標記語言是非法的,那麼該函數會發出一個model_invalid_header的例外。

因此,你的測試內容應該向函數parse_animation_section()傳入一個空指標,
並使用CPPUNIT_ASSERT_THROW巨集,驗證這個函數會拋出model_invalid_header例外。
*/
void TestInvlidHeaderNullValue()
{
model my_model;
CPPUNIT_ASSERT_THROW(my_model.parse_animation_section(0), model_invalid_header);
}
/*
另一個測試是傳入一個有問題的標籤。
*/
void TestInvalidHeaderIllFormatted()
{
model my_model;
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER"
"<VERSION>1.1</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"),
model_invalid_header);
/*
在這標籤測試中,在第一個HEADER的後面少了一個">",並且還缺少了一個結束標記</HEADER>
*/
}
/* 第三個測試應該去測試這個情境:
如果標記中缺少了版本標籤"<VERSION>",或缺少了類型標籤"<TYPE>",這個函數會有怎樣的執行結果呢?

如果沒有找到這些資訊,這個函數就應該相對應的發出model_invalid_version例外和model_invalid_type例外。
*/
void TestInvalidVersionSection()
{
model my_model;
//版本標籤缺少內容
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION></VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);

//缺少版本標籤
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);
//版本標籤出錯
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);
/*
在這標籤測試中,在第一個HEADER的後面少了一個">",並且還缺少了一個結束標記</HEADER>
*/
}
/*
我們還應該建立另一個類似的測試,驗證type類型。
*/
void TestInvalidType()
{
model my_model;
//版本標籤缺少內容
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE></TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);

//缺少版本標籤
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);
//版本標籤出錯
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);
}
正常條件的測試
/* 用失敗的條件去測試固然重要,但是用可以正常工作的資料去檢查函數的正確性也是很重要的。
對於條件測試,我們可以使用CPPUNIT_ASSERT巨集。
這個和C++標準的assert()幾乎一模一樣。

在此要叫函數parse_header_section,然後再來驗證其版本訊息和類型訊息是否正確。
*/
void TestInvalidValidHeader()
{
model my_model;
my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>");
model_invalid_header);

CPPUNIT_ASSERT(my_model.get_version() == 1.2);
CPPUNIT_ASSERT(my_model.get_type() == world_level);
CPPUNIT_ASSERT(strcmp(my_model.get_name(), "Character Select Gallery") == 0);
CPPUNIT_ASSERT(strcmp(my_model.get_author(), "Blake Madden") == 0);

//沒有包含模型的作者和模型名稱
my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"</HEADER>");
model_invalid_header);

CPPUNIT_ASSERT(my_model.get_version() == 1.2);
CPPUNIT_ASSERT(my_model.get_type() == world_level);
CPPUNIT_ASSERT(strcmp(my_model.get_name(), "") == 0);
CPPUNIT_ASSERT(strcmp(my_model.get_author(), "") == 0);
/*
我們傳入函數中的參數是完全合法的標記語言,然後再去確認相對應的版本、類型、名稱以及作者。
我們可以知道哪些成功,至於哪些失敗,CppUnit會把它記錄在Log中。
*/
}
最後,測試治具類別裡的code像這樣
#ifndef MODEL_CLASS_H
#define MODEL_CLASS_H

#include "CppunitLib.h"
#include "model.h"

class ModelTest : public CppUnit::TestFixture
{
public:
/* 第一個測試例子是一個比較簡單的測試:
向這個函數傳遞一個非法的參數,我們就假設去傳入一個空指標。
我們預期的是,如果傳遞給函數的標記語言是非法的,那麼該函數會發出一個model_invalid_header的例外。

因此,你的測試內容應該向函數parse_animation_section()傳入一個空指標,
並使用CPPUNIT_ASSERT_THROW巨集,驗證這個函數會拋出model_invalid_header例外。
*/
void TestInvlidHeaderNullValue()
{
model my_model;
CPPUNIT_ASSERT_THROW(my_model.parse_animation_section(0), model_invalid_header);
}

/*
另一個測試是傳入一個有問題的標籤。
*/
void TestInvalidHeaderIllFormatted()
{
model my_model;
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER"
"<VERSION>1.1</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"),
model_invalid_header);
/*
在這標籤測試中,在第一個HEADER的後面少了一個">",並且還缺少了一個結束標記</HEADER>
*/
}

/* 第三個測試應該去測試這個情境:
如果標記中缺少了版本標籤"<VERSION>",或缺少了類型標籤"<TYPE>",這個函數會有怎樣的執行結果呢?

如果沒有找到這些資訊,這個函數就應該相對應的發出model_invalid_version例外和model_invalid_type例外。
*/
void TestInvalidVersionSection()
{
model my_model;
//版本標籤缺少內容
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION></VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);

//缺少版本標籤
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);
//版本標籤出錯
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_version);
/*
在這標籤測試中,在第一個HEADER的後面少了一個">",並且還缺少了一個結束標記</HEADER>
*/
}

/*
我們還應該建立另一個類似的測試,驗證type類型。
*/
void TestInvalidType()
{
model my_model;
//版本標籤缺少內容
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE></TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);

//缺少版本標籤
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);
//版本標籤出錯
CPPUNIT_ASSERT_THROW(my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>"),
model_invalid_type);
}

/* 用失敗的條件去測試固然重要,但是用可以正常工作的資料去檢查函數的正確性也是很重要的。
對於條件測試,我們可以使用CPPUNIT_ASSERT巨集。
這個和C++標準的assert()幾乎一模一樣。

在此要叫函數parse_header_section,然後再來驗證其版本訊息和類型訊息是否正確。
*/
void TestInvalidValidHeader()
{
model my_model;
my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"<NAME>Character Select Gallery</NAME>"
"<AUTHOR>Blake Madden</AUTHOR>"
"</HEADER>");
model_invalid_header);

CPPUNIT_ASSERT(my_model.get_version() == 1.2);
CPPUNIT_ASSERT(my_model.get_type() == world_level);
CPPUNIT_ASSERT(strcmp(my_model.get_name(), "Character Select Gallery") == 0);
CPPUNIT_ASSERT(strcmp(my_model.get_author(), "Blake Madden") == 0);

//沒有包含模型的作者和模型名稱
my_model.parse_header_section(
"<HEADER>"
"<VERSION>1.2</VERSION>"
"<TYPE>WorldLevel</TYPE>"
"</HEADER>");
model_invalid_header);

CPPUNIT_ASSERT(my_model.get_version() == 1.2);
CPPUNIT_ASSERT(my_model.get_type() == world_level);
CPPUNIT_ASSERT(strcmp(my_model.get_name(), "") == 0);
CPPUNIT_ASSERT(strcmp(my_model.get_author(), "") == 0);
/*
我們傳入函數中的參數是完全合法的標記語言,然後再去確認相對應的版本、類型、名稱以及作者。
我們可以知道哪些成功,至於哪些失敗,CppUnit會把它記錄在Log中。
*/
}
public:
CPPUNIT_TEST_SUITE(ModelTest);
CPPUNIT_TEST(TestInvlidHeaderNullValue);
CPPUNIT_TEST(TestInvalidHeaderIllFormatted);
CPPUNIT_TEST(TestInvalidVersionSection);
CPPUNIT_TEST(TestInvalidType);
CPPUNIT_TEST(TestInvalidValidHeader);
CPPUNIT_TEST_SUITE_END();
};

#endif

沒有留言:

張貼留言