如何打破依赖以启用单元测试

发布于 2024-12-23 11:39:47 字数 6772 浏览 0 评论 0原文

我花了很多时间思考单元测试。我至少购买了有效使用旧代码作为电子书。其中大部分都是有道理的,这似乎是一本关于对旧代码进行单元测试的好书。但我仍然认为我需要一个起点,因为我们的 Attracs 项目很大。另请参阅我关于单元测试的一般问题

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

所以我添加了一个新的测试项目,然后依赖关系造成了麻烦。 我得到了

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

所以总结一下错误

AttracsTest.dpr 使用
使用的 BusinessClasses.pas
使用 BusinessClasses_Interface.inc
Attracs_Interface_Uses.inc

那么如何打破依赖链?

请注意,实际上文件要大得多。模型中有超过 300 个类,businessClasses.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.  

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_Interface_Uses

AttracsDefs,
atXMLObjModel,
XMLObjModel,
XMLParser,
Contnrs,
XMLIntf,
ArrayOfObject,
BoldDBInterfaces,
MsxSupport         // Line that compiler complain about

BusinessClasses_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}

Businessclasses.pas< /strong>

    (*****************************************)
    (*      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

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;

I have spent much time thinking about unittesting. I have at least bought Working Effectively with Legacy Code as an EBook. Most of it make sense, it seems to be a great book about unittesting old code. But still I think I need a starting point for this as our project Attracs is big. See also my generel question about unittesting.

The application has a UML model to define classes, attributes and relations and use Bold for Delphi. After every change in the model we do a roundtrip. This automatically generate declarations for methods in file businessclasses.pas and BusinessClasses_Interface.inc. A SQL-script is also generated if the change require changes in database. This has worked well for years, but we have never used any unittesting.

So I add a new testproject, then the dependencies cause troubles.
I got

[DCC Error] Attracs_Interface_Uses.inc(10): F1026 File not found: 'MsxSupport.dcu'

So to summarize the error

AttracsTest.dpr use
BusinessClasses.pas that use
BusinessClasses_Interface.inc that use
Attracs_Interface_Uses.inc

So how can I break the dependency chain ?

Note that in reality the files are much bigger. There are over 300 classes in the model and businessClasses.pas have over 53000 lines of code...
As a testcase I only have class TPerson with a method AddResponsibility. But you should understand the principle.

Here is my files:

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.  

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_Interface_Uses

AttracsDefs,
atXMLObjModel,
XMLObjModel,
XMLParser,
Contnrs,
XMLIntf,
ArrayOfObject,
BoldDBInterfaces,
MsxSupport         // Line that compiler complain about

BusinessClasses_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}

Businessclasses.pas

    (*****************************************)
    (*      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

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;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

烏雲後面有陽光 2024-12-30 11:39:47

我会做的事情:

  • 对项目进行分支,以便所有更改都可以在安全的“沙箱”
  • 运行中完成 cnWizards 使用 Cleaner(或类似的工具)来清理单元依赖项
  • 将所有必需的单元添加到测试项目 dpr 以记录它们,除了库路径上的第三方库之外,它们是众所周知的依赖项,
  • 根据可用资源制定计划:搜索意外的依赖项(用于更深入的分析)或容易实现的依赖项(可以无风险地删除)。使用限制性构建脚本进行持续集成,该脚本仅指定所需第三方库的库路径,这可以提供很大帮助(每当开发人员引入新的依赖项时,构建就会中断)
  • 时不时地将“安全”更改合并回主干并启动新的使用最新主干版本进行迭代

Things I would do:

  • branch the project so that all changes can be done in a safe "sandbox"
  • run cnWizards Uses Cleaner (or a similar tool) for a cleanup of unit dependencies
  • add all required units to the test project dpr to have them documented, except third party libraries which are on the library path which are well-known dependencies
  • make a plan depending on available resources: search for dependencies which are unexpected (for deeper analysis) or which are low hanging fruits (can be removed without risk). Continuous integration using restrictive build scripts which specify only library paths for required third-party libraries can help a lot (the build will break whenever a developer introduces a new dependency)
  • from time to time merge "safe" changes back to trunk and start a new iteration using the latest trunk version
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文