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

沒有留言:

張貼留言

(什麼是留言欄訊息?)