How to break dependency to enable unittesting

463 views Asked by At

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;
1

There are 1 answers

0
mjn On

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