2

I am mocking an object supporting interface IClient for use in unit testing.

The interface itself is defined in another unit, ClientIF.

The interface references a TDetail defined in another unit, ldetail.

If I mock TDetail, I get an unidentified error on any function that uses TDetail.

[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetDetailValue'
[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetDetail'
[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetActiveDetail'
[DCC Fatal Error] EmailPdfPropertiesGeneratorTests.pas(25): F2063 Could not compile used unit 'MockClient.pas'

I need to be able to mock both the interface and the supported detail object to test just the unit/interface I am working on. Otherwise it all ties to back end data which will be a testing nightmare.

Relevant MockClient.pas unit code

uses ClientIF, MockDetail;

TMockClient = class(TInterfacedObject, IClient)
  FDetail: TDetail;
  function GetDetailValue: TDetail;
  function GetDetail: TDetail;
  function GetActiveDetail: TDetail;
  function GetDetailName: String;
end;

Relevant ClientIF.pas interface code

uses Classes, TaxConst, OSIConst, ldetail, clNotesIF, MissingDataDefIF,
      ClientDataChangeEventIF;

    type
      IClient = interface
        ['{CFED9A10-1601-11D4-ACF6-005004889419}']
        function GetDetailValue: TDetail;
        function GetDetail: TDetail;
        function GetActiveDetail: TDetail;
        function GetDetailName: String;
      end;

Relevant MockDetail.pas code

uses Classes;

type

  TCodeValuesRec = class
    private
      fAmount: double;
      fDesc: String;
      fStateID: integer;
      fCityID: String;
      fSuffixCount: integer;
    public
      property Amount: double read fAmount write fAmount;
      property Desc: String read fDesc write fDesc;
      property StateID: integer read fStateID write fStateID;
      property CityID: String read fCityID write fCityID;
      property SuffixCount: integer read fSuffixCount write fSuffixCount;
  end;

  TDetail = class
  private
    FSeries: Integer;
    FProp: Integer;
    FPropCount: Integer;
    FCodeValuesRec: TCodeValuesRec;
  public
    property Series: Integer read FSeries write FSeries;
    property Prop: Integer read FProp write FProp;
    property PropCount: Integer read FPropCount write FPropCount;
    function GetCodeValuesRecord(WhichRecord: Integer):TCodeValuesRec;
    constructor Create;
    destructor Destroy; override;
  end;

If I replace MockDetail with ldetail in the MockClient uses clause, it compiles, but, of course, the detail is one of the things I have to mock because it is one of the calls from the code under test.

We are trying to bring legacy code under test, which is a process. The code that is having this problem is actually new code, but the first time needed old objects for the test.

The goal for this question is to bring the new code under test, so creating a mock of the old interface (which contains an old class) so that MyClient.GetDetail will return the mock TDetail filled in with information that I can use in the object under test. If there is no way to fake the old code without refactoring it, then the process will have to wait.

If I can get the fake Client and Detail to do their fake work AND compile into the test framework (DUnit) so my tests can run against the real (new) code, that is sufficient, and all that can pursued right now.

We are currently on Delphi 2010 (upgraded this year) and will, EVENTUALLY, be going to the XEs, but I can't use the Mock Framework yet as it appears only to work with XE2.

  • 1
    It doesn't look like you've shown the code that generates the compiler errors. You ought to show that code. And make sure we know exactly where to find MockClient.pas line 16 in your question. The error message is simple. Those three identifiers don't exist in the current scope. You need to declare them. – David Heffernan Dec 05 '12 at 19:49
  • I think Delphi 2010 is going to be too limited to really support mocking, and I sure would not roll your own. You really think it's less work to build a mocking framework in 2010 than move to XE3? Oi vey. – Warren P Dec 05 '12 at 19:49
  • David, I may NOT be showing the code that is causing the problem, but you are seeing exactly what I'm seeing. The actual interface is huge, the cursor after the error actually stops at the declaration of TMockClient, and there is little in the code to generate errors. – Doug Johnson-Cookloose Dec 06 '12 at 12:20
  • Warren, thanks for the sympathy, but in this case, yes. A huge old code base, just finishing up the conversion to 2010 now. Maybe next year for XE3. And maybe as you suggest unit testing with mocking will have to wait. – Doug Johnson-Cookloose Dec 06 '12 at 12:26
  • What I am not seeing is the line numbers in the editor. You can see those. I cannot. So I still do not know which line of code has the errors. – David Heffernan Dec 07 '12 at 19:37
  • David, that's the problem. The line that the cursor ends up on is the actual TMockClient class declaration, not any of the lines of code. The three functions that you see are the ones that "undeclared identifiers" There are no line numbers referenced. There are no line numbers highlighted. There is only the "undeclared identifier" errors (three of them for the functions that have the TDetail class as the return value) and the inability to compile the MockClient.pas file. – Doug Johnson-Cookloose Dec 08 '12 at 21:05

1 Answers1

2

You will have to code against the interface and/or make TDetail an (abstract) ancestor of both the "real" and a mock Detail class. Otherwise you will keep causing rippling effects.

You need to figure out a way to provide whatever is creating the TDetail instances with a means of "injecting" the actual class to use. Think dependency injection, but remember that dependency injection is a concept and not a framework.

To start getting your code less dependent on specific classes, while retaining the ability to use class specific constructors (instead of the standard parameter-less constructor needed by most full blown dependency injection implementations), meta classes can come to the rescue. They allow you to instantiate a class using its own specific constructors.

type
  TDetail = class(TInterfacedObject)
  end;

  TDetailClass = class of TDetail; // Meta class declaration

  function SomeFunction(const aDetailClass: TDetailClass): TDetail;
  begin
    Result := aDetailClass.Create({whatever parameters the TDetail constructor needs});
  end;

One caveat using meta classes and TInterfacedObject descendants: TInterfacedObject has a normal constructor and for instantiation through meta classes to work properly, you really need virtual constructors to ensure that the correct constructor is called. It's that or moving code from constructor overrides in descendants to AfterConstruction overrides.

Note:

Rob is absolutely right that you only need virtual constructors if you need class specific work in a descendant constructor. My "advise" is based less on the specifics of TDetail in this example and more on the merit of having a TInterfacedObject "replacement" with a virtual constructor if you are moving toward more inversion of control through dependency injection and mocking (or stubbing).

Note:

Making the fake stuff work without any changes upstream is going to be a tall order. However, you might get away with an approach using Delphi's scoping order: naming your test stubs (or mocks) exactly the same as your classes under test and making sure that the mock units are closer in scope for the unit creating the TDetail instances than the unit containing the actual definition:

uses 
  Client, Detail, MockClient, MockDetail;

would ensure that TDetail.Create instantiates the TDetail from the MockDetail unit instead of the Detail unit.

Marjan Venema
  • 19,136
  • 6
  • 65
  • 79
  • You only need virtual constructors if you need to invoke the constructor polymorphically. If descendants of `TDetail` don't need to do their own class-specific work in the constructor, then the hierarchy doesn't need a virtual constructor. That doesn't mean they all have to use the base `TInterfacedObject` constructor, though. `TDetail` can provide its own constructor, and all descendants will simply use that. – Rob Kennedy Dec 05 '12 at 20:05
  • @RobKennedy: absolutely. See note I added. – Marjan Venema Dec 05 '12 at 20:26
  • I'm in agreement, but this appears to require refactoring upstream and that is not an available option for me. I've modified the question specifically to indicate that my goal is to make the fake stuff work (eliminate the compiler error) for testing something else. However, I will be taking what you've both said under advisement for refactoring work next year. Unless I'm missing something (likely). – Doug Johnson-Cookloose Dec 06 '12 at 12:46
  • @DougJohnson: scope order might be something you can put to use. See second note. – Marjan Venema Dec 06 '12 at 13:16