Unit testing 如何打破依赖关系以启用单元测试

Unit testing 如何打破依赖关系以启用单元测试,unit-testing,delphi,dependencies,dunit,bold-delphi,Unit Testing,Delphi,Dependencies,Dunit,Bold Delphi,我花了很多时间考虑单元测试。我至少买了一本电子书。大部分内容都很有意义,它似乎是一本关于单元测试旧代码的好书。但我仍然认为我需要一个起点,因为我们的Attracs项目很大。另请参见我关于单元测试的概述 该应用程序有一个UML模型来定义类、属性和关系,并使用Bold for Delphi。模型每次更改后,我们都会往返一次。这会自动为文件businessclasses.pas和businessclasses_Interface.inc中的方法生成声明。如果更改需要在数据库中进行更改,则还会生成SQL

我花了很多时间考虑单元测试。我至少买了一本电子书。大部分内容都很有意义,它似乎是一本关于单元测试旧代码的好书。但我仍然认为我需要一个起点,因为我们的Attracs项目很大。另请参见我关于单元测试的概述

该应用程序有一个UML模型来定义类、属性和关系,并使用Bold for Delphi。模型每次更改后,我们都会往返一次。这会自动为文件businessclasses.pas和businessclasses_Interface.inc中的方法生成声明。如果更改需要在数据库中进行更改,则还会生成SQL脚本。这已经很好地工作了很多年,但我们从未使用过任何单元测试

因此,我添加了一个新的testproject,然后依赖项会带来麻烦。 我得到

[DCC错误]Attracs_Interface_Uses.inc(10):未找到F1026文件:“MsxSupport.dcu”

总结一下错误

AttracsTest.dpr使用
使用
使用
Attracs_Interface_Uses.inc.公司

那么如何打破依赖链呢?

请注意,实际上文件要大得多。模型和BusinessClass中有300多个类。pas有53000多行代码。。。 作为一个测试用例,我只有带有AddResponsibility方法的类TPerson。但是你应该理解这个原则

这是我的档案:

AttracsTest.dpr

program AttracsTests;
{$IFDEF CONSOLE_TESTRUNNER}
{$APPTYPE CONSOLE}
{$ENDIF}
uses
  Forms,
  TestFramework,
  GUITestRunner,
  TextTestRunner,
  BusinessClasses in '..\..\server\code\BusinessClasses.pas',
  TestBusinessClasses in 'TestBusinessClasses.pas',
  ArrayOfObject in '..\..\server\code\ArrayOfObject.pas';

{$R *.RES}

begin
  Application.Initialize;
  if IsConsole then
    TextTestRunner.RunRegisteredTests
  else
    GUITestRunner.RunRegisteredTests;
end.  
AttracsDefs,
atXMLObjModel,
XMLObjModel,
XMLParser,
Contnrs,
XMLIntf,
ArrayOfObject,
BoldDBInterfaces,
MsxSupport         // Line that compiler complain about
    (*****************************************)
    (*      This file is autogenerated       *)
    (*   Any manual changes will be LOST!    *)
    (*****************************************)

    unit BusinessClasses;

    {$DEFINE BusinessClasses_unitheader}
    {$INCLUDE BusinessClasses_Interface.inc}

    { Includefile for methodimplementations 
      Have concrete implementation of methods}
    {$INCLUDE Person.inc}

    // Some get and set methods fopr attributes in the class

    // attribute FirstName
    function TPerson._Get_M_FirstName: TBAString;
    begin
      assert(ValidateMember('TPerson', 'FirstName', 14, TBAString));
      Result := TBAString(BoldMembers[14]);
    end;

    function TPerson._GetFirstName: String;
    begin
      Result := M_FirstName.AsString;
    end;

    procedure TPerson._SetFirstName(const NewValue: String);
    begin
      M_FirstName.AsString := NewValue;
    end;

    procedure InstallBusinessClasses(BoldObjectClasses: TBoldGeneratedClassList);
    begin
      BoldObjectClasses.AddObjectEntry('Person', TPerson);
    end;

    var
      CodeDescriptor: TBoldGeneratedCodeDescriptor;

    initialization
      CodeDescriptor := GeneratedCodes.AddGeneratedCodeDescriptorWithFunc('BusinessClasses', InstallBusinessClasses, InstallObjectListClasses, GeneratedCodeCRC);
    finalization
      GeneratedCodes.Remove(CodeDescriptor);
    end.
TestBusinessClasses.pas

unit TestBusinessClasses;

interface

uses
  TestFramework,
  ArrayOfObject,
  AttracsAttributes,
  AttracsDefs,
  atXMLObjModel,
  BoldAttributes
  BoldDBInterfaces,
  BoldDefs,
  BoldDeriver,
  BoldDomainElement,
  BoldElements,
  BoldSubscription,
  BoldSystem,
  BoldSystemRT,
  BusinessClasses,   // Trigger the dependency, but also contain info about the classes get and set methods for attributes.  
  Classes,
  Contnrs,
  SysUtils,
  XMLIntf,
  XMLObjModel,
  XMLParser;

type
  TestTPerson = class(TTestCase)
  strict private
    FPerson: TPerson;
  public
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestAddResponsibility;
  end;

implementation

procedure TestTPerson.SetUp;
begin
  FPerson := TPerson.Create;
end;

procedure TestTPerson.TearDown;
begin
  FPerson.Free;
  FPerson := nil;
end;

procedure TestTPerson.TestAddResponsibility;
var
  ReturnValue: Boolean;
  aSession: TLogonSession;
  aDevType: TDevTypeDef;
  aMarketArea: TMarketArea;
begin
  // TODO: Setup method call parameters
  ReturnValue := FPerson.AddResponsibility(aMarketArea, aDevType, aSession);
  // TODO: Validate method results
end;

initialization
  // Register any test cases with the test runner
  RegisterTest(TestTPerson.Suite);
end.
Attracs\u接口\u使用

program AttracsTests;
{$IFDEF CONSOLE_TESTRUNNER}
{$APPTYPE CONSOLE}
{$ENDIF}
uses
  Forms,
  TestFramework,
  GUITestRunner,
  TextTestRunner,
  BusinessClasses in '..\..\server\code\BusinessClasses.pas',
  TestBusinessClasses in 'TestBusinessClasses.pas',
  ArrayOfObject in '..\..\server\code\ArrayOfObject.pas';

{$R *.RES}

begin
  Application.Initialize;
  if IsConsole then
    TextTestRunner.RunRegisteredTests
  else
    GUITestRunner.RunRegisteredTests;
end.  
AttracsDefs,
atXMLObjModel,
XMLObjModel,
XMLParser,
Contnrs,
XMLIntf,
ArrayOfObject,
BoldDBInterfaces,
MsxSupport         // Line that compiler complain about
    (*****************************************)
    (*      This file is autogenerated       *)
    (*   Any manual changes will be LOST!    *)
    (*****************************************)

    unit BusinessClasses;

    {$DEFINE BusinessClasses_unitheader}
    {$INCLUDE BusinessClasses_Interface.inc}

    { Includefile for methodimplementations 
      Have concrete implementation of methods}
    {$INCLUDE Person.inc}

    // Some get and set methods fopr attributes in the class

    // attribute FirstName
    function TPerson._Get_M_FirstName: TBAString;
    begin
      assert(ValidateMember('TPerson', 'FirstName', 14, TBAString));
      Result := TBAString(BoldMembers[14]);
    end;

    function TPerson._GetFirstName: String;
    begin
      Result := M_FirstName.AsString;
    end;

    procedure TPerson._SetFirstName(const NewValue: String);
    begin
      M_FirstName.AsString := NewValue;
    end;

    procedure InstallBusinessClasses(BoldObjectClasses: TBoldGeneratedClassList);
    begin
      BoldObjectClasses.AddObjectEntry('Person', TPerson);
    end;

    var
      CodeDescriptor: TBoldGeneratedCodeDescriptor;

    initialization
      CodeDescriptor := GeneratedCodes.AddGeneratedCodeDescriptorWithFunc('BusinessClasses', InstallBusinessClasses, InstallObjectListClasses, GeneratedCodeCRC);
    finalization
      GeneratedCodes.Remove(CodeDescriptor);
    end.
BusinessClasses\u Interface.inc

(*****************************************)
(*      This file is autogenerated       *)
(*   Any manual changes will be LOST!    *)
(*****************************************)

{$IFNDEF BusinessClasses_Interface.inc}
{$DEFINE BusinessClasses_Interface.inc}

{$IFNDEF BusinessClasses_unitheader}
unit BusinessClasses;
{$ENDIF}

{$INCLUDE Attracs.inc} //PATCH

interface

uses
  // interface uses
  {$INCLUDE Attracs_Interface_Uses.inc} ,
  // interface dependencies
  // attribute classes
  AttracsAttributes,
  BoldAttributes,
  // other
  Classes,
  SysUtils,
  BoldDefs,
  BoldSubscription,
  BoldDeriver,
  BoldElements,
  BoldDomainElement,
  BoldSystemRT,
  BoldSystem;

type
  { forward declarations of all classes }
  TPerson = class;

  TPerson = class(TAmStateObject)
  public
    function AddResponsibility(aMarketArea: TMarketArea; aDevType: TDevTypeDef; aSession: TLogonSession): Boolean; 
  end;

function GeneratedCodeCRC: String;

implementation

uses
  // implementation uses
  {$INCLUDE Attracs_Implementation_Uses.inc} ,
  // implementation dependencies
  // other
  BoldGeneratedCodeDictionary;

{$ENDIF}
function TPerson.AddResponsibility(aMarketArea: TMarketArea; aDevType: TDevTypeDef; aSession: TLogonSession): Boolean;
var
  vOCL: String;
  vDevResponse: TDevResponsible;
begin
  vOCL := Format('DevResponsible.allinstances->select((devType.TypeName = ''%s'') and (marketArea.name = ''%s''))->first',
                         [aDevType.TypeName, aMarketArea.name]);
  vDevResponse := GetApplicationKernel.EvaluateExpressionAsDirectElement(vOCL) as TDevResponsible;

  if not Assigned(vDevResponse) then
    vDevResponse := GetApplicationKernel.CreateAMObject('DevResponsible') as TDevResponsible;

  if Assigned(vDevResponse) then
  begin
    vDevResponse.marketArea := aMarketArea;
    vDevResponse.devType := aDevType;
    vDevResponse.responsiblePers := self;
    NotifyModificationHistory(Now, aSession, Format('Responsible for %s marketarea: %s', [aDevType.TypeName, aMarketArea.Name]));
    Result := True;
  end
  else
    Result := False;
end;
商务舱。pas

program AttracsTests;
{$IFDEF CONSOLE_TESTRUNNER}
{$APPTYPE CONSOLE}
{$ENDIF}
uses
  Forms,
  TestFramework,
  GUITestRunner,
  TextTestRunner,
  BusinessClasses in '..\..\server\code\BusinessClasses.pas',
  TestBusinessClasses in 'TestBusinessClasses.pas',
  ArrayOfObject in '..\..\server\code\ArrayOfObject.pas';

{$R *.RES}

begin
  Application.Initialize;
  if IsConsole then
    TextTestRunner.RunRegisteredTests
  else
    GUITestRunner.RunRegisteredTests;
end.  
AttracsDefs,
atXMLObjModel,
XMLObjModel,
XMLParser,
Contnrs,
XMLIntf,
ArrayOfObject,
BoldDBInterfaces,
MsxSupport         // Line that compiler complain about
    (*****************************************)
    (*      This file is autogenerated       *)
    (*   Any manual changes will be LOST!    *)
    (*****************************************)

    unit BusinessClasses;

    {$DEFINE BusinessClasses_unitheader}
    {$INCLUDE BusinessClasses_Interface.inc}

    { Includefile for methodimplementations 
      Have concrete implementation of methods}
    {$INCLUDE Person.inc}

    // Some get and set methods fopr attributes in the class

    // attribute FirstName
    function TPerson._Get_M_FirstName: TBAString;
    begin
      assert(ValidateMember('TPerson', 'FirstName', 14, TBAString));
      Result := TBAString(BoldMembers[14]);
    end;

    function TPerson._GetFirstName: String;
    begin
      Result := M_FirstName.AsString;
    end;

    procedure TPerson._SetFirstName(const NewValue: String);
    begin
      M_FirstName.AsString := NewValue;
    end;

    procedure InstallBusinessClasses(BoldObjectClasses: TBoldGeneratedClassList);
    begin
      BoldObjectClasses.AddObjectEntry('Person', TPerson);
    end;

    var
      CodeDescriptor: TBoldGeneratedCodeDescriptor;

    initialization
      CodeDescriptor := GeneratedCodes.AddGeneratedCodeDescriptorWithFunc('BusinessClasses', InstallBusinessClasses, InstallObjectListClasses, GeneratedCodeCRC);
    finalization
      GeneratedCodes.Remove(CodeDescriptor);
    end.
person.inc

(*****************************************)
(*      This file is autogenerated       *)
(*   Any manual changes will be LOST!    *)
(*****************************************)

{$IFNDEF BusinessClasses_Interface.inc}
{$DEFINE BusinessClasses_Interface.inc}

{$IFNDEF BusinessClasses_unitheader}
unit BusinessClasses;
{$ENDIF}

{$INCLUDE Attracs.inc} //PATCH

interface

uses
  // interface uses
  {$INCLUDE Attracs_Interface_Uses.inc} ,
  // interface dependencies
  // attribute classes
  AttracsAttributes,
  BoldAttributes,
  // other
  Classes,
  SysUtils,
  BoldDefs,
  BoldSubscription,
  BoldDeriver,
  BoldElements,
  BoldDomainElement,
  BoldSystemRT,
  BoldSystem;

type
  { forward declarations of all classes }
  TPerson = class;

  TPerson = class(TAmStateObject)
  public
    function AddResponsibility(aMarketArea: TMarketArea; aDevType: TDevTypeDef; aSession: TLogonSession): Boolean; 
  end;

function GeneratedCodeCRC: String;

implementation

uses
  // implementation uses
  {$INCLUDE Attracs_Implementation_Uses.inc} ,
  // implementation dependencies
  // other
  BoldGeneratedCodeDictionary;

{$ENDIF}
function TPerson.AddResponsibility(aMarketArea: TMarketArea; aDevType: TDevTypeDef; aSession: TLogonSession): Boolean;
var
  vOCL: String;
  vDevResponse: TDevResponsible;
begin
  vOCL := Format('DevResponsible.allinstances->select((devType.TypeName = ''%s'') and (marketArea.name = ''%s''))->first',
                         [aDevType.TypeName, aMarketArea.name]);
  vDevResponse := GetApplicationKernel.EvaluateExpressionAsDirectElement(vOCL) as TDevResponsible;

  if not Assigned(vDevResponse) then
    vDevResponse := GetApplicationKernel.CreateAMObject('DevResponsible') as TDevResponsible;

  if Assigned(vDevResponse) then
  begin
    vDevResponse.marketArea := aMarketArea;
    vDevResponse.devType := aDevType;
    vDevResponse.responsiblePers := self;
    NotifyModificationHistory(Now, aSession, Format('Responsible for %s marketarea: %s', [aDevType.TypeName, aMarketArea.Name]));
    Result := True;
  end
  else
    Result := False;
end;
我会做的事情:

  • 分支项目,以便所有更改都可以在安全的“沙箱”中完成
  • 运行cnWizards使用Cleaner(或类似工具)清理单元依赖项
  • 将所有必需的单元添加到测试项目dpr中,以对其进行文档化,但位于库路径上的第三方库除外,这些库是众所周知的依赖项
  • 根据可用资源制定计划:搜索意外的依赖项(用于更深入的分析)或低效的依赖项(可以在没有风险的情况下删除)。使用仅为所需第三方库指定库路径的限制性构建脚本进行持续集成会有很大帮助(每当开发人员引入新的依赖项时,构建就会中断)
  • 不时地将“安全”更改合并回主干,并使用最新的主干版本开始新的迭代

MsxSupport是一个可怕的依赖项,在测试中不起作用吗?或者问题只是编译器找不到它?如果它在测试中不起作用,那么您的“接口”单元首先就不应该依赖它——您可能需要另一层间接寻址。如果编译器找不到它,那么您所需要做的就是更新测试项目的搜索路径或包列表。此时,我不会太担心您需要向测试项目添加哪些单元,将它们全部添加或使用搜索路径。此时,我应该尝试限制对象之间的依赖关系。要测试您的TPerson,您不应该首先创建无数其他对象。如果您这样做了,请首先尝试解决这些依赖关系(DI某人?)。最后,这也将减轻一些单元间的依赖性,但正如我所说的,我不会太担心这些。推荐阅读:Gerard Meszaros的xUnit测试模式:重构测试代码