当前位置: 首页 > 工具软件 > DUnit > 使用案例 >

DUnit初步学习及TTestCase中的方法详解

西门洛城
2023-12-01

一、简介

   DUnit是xUnit家族成员之一,源于著名的回归测试框架JUnit,由Juanco设计成Delphi版本,可以从dunit.sourceforge.net网站免费获得,最新版本9.2.1。获得dunit-9.2.1.zip文件后,解压缩到指定文件夹。


二、配置类库

   开发工具我使用的是Borland的Delphi 2006(正确的叫法应该是Borland Developer Studio 2006,以下简称BDS)。虽然BDS自带了DUnit,且通过“New Items”对话框中“Unit Test”下的各项就可以建立测试工程和测试用例,但这里将直接使用解压后的文件。此外本文将同时介绍如何在C++Builder和Delphi中使用DUnit,其中C++Builder部分源于猛禽的《在BCB中使用DUnit》。


   打开BDS后,点击菜单“File”->“New”->“Other”,打开“New Items”对话框:



   在该对话框中,选择“C++Builder Projects”或“Delphi Projects”,然后选择其中的“VCL Forms Application”,点击“OK”按钮。项目创建后先关闭Form1,由于此时项目尚未保存,BDS会提示是否保存,选择“No”,不保存,因为这里不需要Form1,需要的只是GUI运行环境。下面点击菜单“File”->“Save All”,在弹出的对话框中设置项目文件名称及保存位置,项目文件名称即项目名称,我的项目文件名称分别为NUnitCB.bdsproj和NUnitOP.bdsproj,并保存在本机的G:/YPJCCK/DUnit/Delphi/DUnitCB和G:/YPJCCK/DUnit/Delphi/DUnitOP文件夹中。


   项目创建后,点击菜单“Project”->“Options”,弹出对话框:



   在对话框的树型菜单中选择“Paths and Define”,然后在窗口右边选择与“Include search path”对应的“Edit”按钮,打开“Path”对话框:



   请使用“…”按钮选择DUnit源码存放路径F:/dunit-9.2.1/src,再使用“Add”按钮将该路径添加到列表中。添加成功后点击“OK”按钮返回之前的窗口,再点击“OK”按钮返回项目。这是在C++Builder中。如果是在Delphi中,点击菜单“Project”->“Options”后弹出对话框如下:



   在对话框的树型菜单中选择“Directories/Conditionals”,然后在窗口右边选择与“Search path”对应的“…”按钮,打开“Directories”对话框:



   接下来的操作与之前一样。


   设置好后,请点击菜单“Project”->“Add to Project”,将F:/dunit-9.2.1/src下的GUITestRunner.pas和TestFramework.pas文件添加到项目中。添加的时候,BDS可能会提示错误,相信我,不要管它。此外,如果是在C++Builder中,添加完成后请按下F9键,使程序运行一遍,以生成GUITestRunner.hpp和TestFramework.hpp文件,这两个文件将在后边用到。


三、编写用于测试的类

   用于测试的类很简单,名为TBook,只有id和name两个属性,这两个属性将分别用于两个用例当中。


   下面开始编写,请点击菜单“File”->“New”->“Other”,打开“New Items”对话框:



   在该对话框中选择“C++Builder Projects” 下的“C++Builder Files”或“Delphi Projects”下的“Delphi Files”,然后选中“Unit”,点击“OK”按钮。此时Unit文件虽然在工程中已生成,但尚未保存在硬盘上,所以请先按下快捷键Ctrl + S,我将文件命名为Book.cpp和Book.pas。


  文件创建后,需要修改代码,下边是C++Builder代码:
  Book.h文件:
  //---------------------------------------------------------------------------
  #ifndef BookH
  #define BookH
  //---------------------------------------------------------------------------
  #include
  class TBook
  {
  private:
    AnsiString pid;
    AnsiString pname;

    AnsiString GetId();
    void SetId(AnsiString value);

    AnsiString GetName();
    void SetName(AnsiString value);
  public:
    __property AnsiString id = {read = GetId, write = SetId};
    __property AnsiString name = {read = GetName, write = SetName};
  };
  #endif
  Book.cpp文件:
  //---------------------------------------------------------------------------
  #pragma hdrstop
  #include "Book.h"
  //---------------------------------------------------------------------------
  #pragma package(smart_init)
  AnsiString TBook::GetId()
  {
    return pid;
  }
  void TBook::SetId(AnsiString value)
  {
    pid = value;
  }

  AnsiString TBook::GetName()
  {
    return pname;
  }
  void TBook::SetName(AnsiString value)
  {
    pname = value;
  }
   这里可能引起费解的是属性,属性是Borland对C++做的扩展,在C++Builder中定义属性要先为这个属性声明一个作为read访问器的函数和一个作为write访问器的过程,并实现之,然后使用__property关键字来定义属性,并将read访问器和write访问器与这两个函数和过程相关联。注意,这里建议将函数和过程声明在private部分,这样在调用时就会只看到属性,而不会看到这两个函数和过程了。


  下边是Delphi代码:
  unit Book;

  interface

  type
    TBook = class
    private
      pid : string;
      pname : string;

      function GetId():string;
      procedure SetId(value: string);

      function GetName():string;
      procedure SetName(value: string);
    public
      constructor Create;
      property id : string read GetId write SetId;
      property name : string read GetName write SetName;
    end;

  implementation

  constructor TBook.Create;
  begin
    inherited Create;
  end;

  function TBook.GetId():string;
  begin
    GetId := pid;
  end;
  procedure TBook.SetId(value: string);
  begin
    pid := value;
  end;

  function TBook.GetName():string;
  begin
    GetName := pname;
  end;
  procedure TBook.SetName(value: string);
  begin
    pname := value;
  end;
  end.
   这里可能引起费解的还是属性,这在前边已经介绍过,Delphi与之类似,只不过关键字是property。另外,别忘了定义一个构造函数,Delphi是不会默认提供的。


  至此,用于测试的类编写完成。


四、编写测试用例

  这里只用了一个类进行测试,名为TBookTest,该类继承自TTestCase类。TBookTest类包含两个用例,分别对应该类的testId和testName方法,即每个方法实现了一个测试用例。注意,在DUnit中,对于测试方法的名称并没有特殊要求,但要求其访问符必须为__published(C++Builder)和published(Delphi),所有以__published和published关键字修饰的方法都将被视为测试方法。此外,TBookTest还包括SetUp和TearDown这两个方法,前者在每个测试方法开始之前执行,多用来做初始化;后者在每个测试方法完成之后执行,多用来清理资源。下面开始编写TBookTest。


  点击菜单“File”->“New”->“Other”,打开“New Items”对话框,在该对话框中选择“C++Builder Projects” 下的“C++Builder Files”或“Delphi Projects”下的“Delphi Files”,然后选中“Unit”,点击“OK”按钮。此时请按下快捷键Ctrl + S,保存文件,我将文件命名为TBookTest.cpp和TBookTest.pas。


  下面修改代码,C++Builder代码如下:
  BookTest.h文件:
  //---------------------------------------------------------------------------
  #ifndef BookTestH
  #define BookTestH
  //---------------------------------------------------------------------------
  #include "Book.h"
  #include "testframework.hpp"
  class TBookTest : TTestCase
  {
    TBook *book;
  protected:
    void __fastcall SetUp();
    void __fastcall TearDown();
  __published:
    void __fastcall testId();
    void __fastcall testName();
  };
  #endif
  BookTest.cpp文件:
  //---------------------------------------------------------------------------
  #pragma hdrstop
  #include "BookTest.h"
  //---------------------------------------------------------------------------
  #pragma package(smart_init)
  void __fastcall TBookTest::SetUp()
  {
    book = new TBook();
  }

  void __fastcall TBookTest::TearDown()
  {
    book = NULL;
  }

  void __fastcall TBookTest::testId()
  {
    book->id = "001"; //设置id属性的值为001
    //使用Check查看id属性的值是否为001
    Check(book->id ="=" "001", "id属性被测试!");
  }

  void __fastcall TBookTest::testName()
  {
    book->name = "ASP"; //设置name属性的值为ASP
    //使用Check查看name属性的值是否为JSP,这是个必然出现错误的测试
    Check(book->name ="=" "JSP", "name属性被测试!");
  }
   这里SetUp和TearDown方法没什么好说的,就是执行了对book对象的初始化和清理,不过testId和testName需要说明一下。前者是在对book的id属性进行测试,首先赋值为”001”,然后使用Check方法查看id属性与期待值的比较结果,由于我的期待值也是”001”,所以比较结果为true,执行后这个用例是成功的;后者则是对book的name属性进行测试,也是先赋值为”ASP”,然后使用Check方法查看其值与期待值的比较结果,由于我特意将期待值设定为根本不可能的”JSP”,因此比较结果为false,这个用例执行后会出现一个错误。但请注意,由于我是特意要让测试出现错误,所以将期待值设定成了不可能的值,如果你是测试人员,请千万不要这么做,否则如果别的地方导致了错误,很容易给自己造成不必要的麻烦。


  与JUnit、NUnit不同,DUnit中没有提供Assert类,相关的方法都被定义到了TTestCase类中,由于测试类都要继承TTestCase,因此在测试类中可以直接调用这些方法。方法有26个:

  1.Check()方法,最常用的方法,用来查看表达式是否为true,为true则测试成功,反之失败。

  2.CheckTrue()和CheckFalse()方法,用来查看变量是否为false或true,如果CheckFalse ()查看的变量的值是false则测试成功,如果是true则失败,CheckTrue ()与之相反。

  3.CheckEquals()和CheckNotEquals()方法,用来查看两个对象的值是否相等或不等。

  4.CheckEqualsBin()、CheckNotEqualsBin()方法,用来比较两个无符号32位整数,并在输出信息时以二进制格式输出数值。

  5.CheckEqualsHex()和CheckNotEqualsHex()方法,用来比较两个无符号32位整数,并在输出信息时以十六进制格式输出数值。

  6.CheckEqualsMem()和CheckNotEqualsMem()方法,按指定范围Length比较两个指针所指向的内存中所保存的内容是否相同或不同,可以参考Delphi中的CompareMem函数。

  7.CheckEqualsString()和CheckNotEqualsString()方法,比较两个字符串是否相同。

  8.CheckEqualsWideString()和CheckNotEqualsWideString()方法,比较两个字符串是否相同,该字符串以Unicode格式编码。

  9.CheckNull()和CheckNotNull()方法,查看对象是否为空和不为空。

  10.CheckSame()方法,用来比较两个对象是否指向同一内存地址。

  11.CheckException()方法,用来查看指定测试方法返回的异常与指定的异常类型是否相同。

  12.CheckInherits()方法,查看指定的前一个类型是否是后一个类型的基类。例如:
CheckInherits(TObject, TTestCase);

  13.CheckIs()方法,查看对象的类型是否兼容于指定类型,例如TObject是VCL中所有类型的基类,因此所有类型的对象都兼容于TObject。

  14.CheckMethodIsNotEmpty()方法,查看方法是否非空,即方法中是否有代码。

  15.Fail()方法,意为失败,用来抛出错误。我个人认为有两个用途:首先是在测试驱动开发中,由于测试用例都是在被测试的类之前编写,而写成时又不清楚其正确与否,此时就可以使用Fail方法抛出错误进行模拟;其次是抛出意外的错误,比如要测试的内容是从数据库中读取的数据是否正确,而导致错误的原因却是数据库连接失败。

  16.FailEquals()、FailNotEquals()和FailNotSame()方法,功能与Fail方法一样,但带格式,很多Check()方法都直接使用了这些方法。


  下面是Delphi代码:
  unit BookTest;

  interface

  uses
    Book,
    TestFramework;

  type
    TBookTest = class(TTestCase)
    book : TBook;
    protected
      procedure SetUp; override;
      procedure TearDown; override;
    published
      procedure testId;
      procedure testName;
    end;

  implementation

  procedure TBookTest.SetUp;
  begin
    book := TBook.Create();
  end;

  procedure TBookTest.TearDown;
  begin
    book.Free;
  end;

  procedure TBookTest.testId;
  begin
    book.id := '001'; //设置id属性的值为001
    //使用Check查看id属性的值是否为001
    Check(book.id = '001', 'id属性被测试!');
  end;

  procedure TBookTest.testName;
  begin
    book.name := 'ASP'; //设置name属性的值为ASP
    //使用Check查看name属性的值是否为JSP,这是个必然出现错误的测试
    Check(book.name = 'JSP', 'name属性被测试!');
  end;

  end.
   至此,测试类创建完成。


五、运行DUnit

  要运行DUnit,在程序中写代码就可以达成。请在BookTest.h文件的最后一行代码#endif之前增加代码“_di_ITest __fastcall GetSuite(TMetaClass * aClass);”,然后在BookTest.cpp中追加代码:


  //用于替代TTestCase类的Suite方法,因为该方法使用了C++不支持的特性
  _di_ITest __fastcall GetSuite(TMetaClass *aClass)
  {
    _di_ITest Result;
    if (!Supports(new TTestSuite(aClass), __uuidof(ITest), &Result))
      throw Exception("Interface ITest not supported");
    return Result;
  }

  //模拟Delphi中的initialization
  class Initialization
  {
  public :
    Initialization()
    {
      RegisterTest(GetSuite(__classid(TBookTest)));
    }
  };
  Initialization initialization;


   最后修改DUnitCB.cpp文件如下:
  //---------------------------------------------------------------------------
  #include
  #include "TestFramework.hpp"
  #include "GUITestRunner.hpp"
  #pragma hdrstop
  //---------------------------------------------------------------------------
  WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
  {
    try
    {
      Application->Initialize();
      //Application->Run();
      Guitestrunner::RunRegisteredTests();
    }
    catch (Exception &exception)
    {
      Application->ShowException(&exception);
    }
    catch (...)
    {
      try
      {
        throw Exception("");
      }
      catch (Exception &exception)
      {
        Application->ShowException(&exception);
      }
    }
     return 0;
  }
  //---------------------------------------------------------------------------
   这是在C++Builder中。如果是在Delphi中,请将BookTest.pas的最后一行代码“end.”修改为:
  initialization
  RegisterTest(TBookTest.Suite);
  end.
   然后修改DUnitOP.dpr文件如下:
  program DUnitOP;

  uses
    Forms,
    TestFramework in 'F:/dunit-9.2.1/src/TestFramework.pas',
    GUITestRunner in 'F:/dunit-9.2.1/src/GUITestRunner.pas',
    Book in 'Book.pas',
    BookTest in 'BookTest.pas';

  {$R *.res}

  begin
    Application.Initialize;
    //Application.Run;
    GUITestRunner.RunRegisteredTests;
  end.
   注意,如果看不到C++Builder中的DUnitCB.cpp或Delphi中的DUnitOP.dpr文件,请点击菜单“Project”->“View Source”。改好后,点击菜单“Run”->“Run”或按F9键运行程序,就可以使用看到DUnit界面了:



   此时就可以使用TBookTest类对TBook类进行测试了。点击“Run”按钮,运行结果如下图:



testId前的点是绿色,而testName前的点是红色,且进度条也显示为红条,这表明testName中存在错误。不过这个错误是预计之内的,如果不想看到,可以在BDS中将testName()方法中的”JSP”改成”ASP”,然后重新运行,此时进度条已不是红色,而是绿色了。


六、测试套件

  当有多个测试类需要按结构分组进行测试时,可以使用测试套件来完成这项工作。DUnit的测试套件就是返回类型为ITestSuite的函数,如下:
  //使用suite的AddTest函数添加测试
  function Suite1: ITestSuite;
  var
    suite: TTestSuite;
  begin
    suite := TTestSuite.Create('TestUnit Suite');
    suite.AddTest(TBookTest.Suite);
    Result := suite;
  end;

  //在创建的同时添加测试
  function Suite2: ITestSuite;
  begin
    Result := TTestSuite.Create('TestUnit Suite', [TBookTest.Suite]);
  end;
   以上是Delphi代码,C++Builder代码如下:
  //使用suite的AddTest函数添加测试
  _di_ITest __fastcall Suite1()
  {
    TTestSuite* suite;
    suite = new TTestSuite("TestUnit Suite");
    suite->AddTest(GetSuite(__classid(TBookTest)));
    return *suite;
  }

  //在创建的同时添加测试
  _di_ITest __fastcall Suite2()
  {
    _di_ITest ts[] = {GetSuite(__classid(TBookTest))};
    return *(new TTestSuite("TestUnit Suite", ts, 1));
  }


七、更多运行方法

  要运行DUnit测试,还可以更灵活,例如将“RegisterTest(TBookTest.Suite);”这行代码直接写到DUnitOP.dpr文件中、“GUITestRunner.RunRegisteredTests;”之前(此为Delphi代码,C++Builder代码为“RegisterTest(GetSuite(__classid(TTestSF)));”,应写到DUnitCB.cpp文件中、Guitestrunner::RunRegisteredTests();之前),或使用GUITestRunner的RunTest方法。


  此外,DUnit也提供有命令行测试环境,要调用该环境则需建立“Console Application”,并引用F:/dunit-9.2.1/src文件夹下的TextTestRunner.pas文件,具体使用方法可以参考GUITestRunner。

 类似资料: