Jump to content
Dmitry Onoshko

Two versions of a unit with different $DEFINEs in the same project

Recommended Posts

I guess, I know the answer but maybe I miss something. The question arose related to unit testing with DUnitX but is not really tied to it.

 

Say, we have a Delphi unit shared between multiple projects in a project group, and certain code pieces in the unit are written in $IFDEFs to exclude them in one of the projects and include for the rest (say, for performance reasons or to remove some stuff in a program for outside world use). The code pieces in question are part of quite complex algorithm(s), so unit testing is a must. I understand one can just create another build configuration for the DUnitX project, or even create a separate test project, but that’s not quite convenient: being able to test the whole unit in all its variant with a single test project would be much better.

 

Another use case I can imagine is something related to supporting different versions of some protocol or format in the same executable without the risk of introducing bugs by merging several implementations into a single one (although this could probably be done by giving unit for different versions different names). So, let’s just stick to testing $IFDEF-dependent unit as a whole in one test project as the use case.

 

I guess, linker and separate unit compilation are one of the problems here, but… Is that even possible?

Share this post


Link to post

You could use includes from different units for testing.

 

Lets say this is the unit you want to test:

{$IFNDEF UNIT_TESTING}
unit ToTest;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;
function NotAlwaysAvaialble(): TSomeOtherType;

implementation

uses
  Other,
  Units;

{$ENDIF}

function AlwaysAvailable(): TSomeType;
begin
  // implementation here
end;

{$IFDEF USE_EVERYTHING}
function NotAlwaysAvaialble(): TSomeOtherType;
begin
  // implementation here
end;
{$ENDIF}

{$IFNDEF UNIT_TESTING}
end.
{$ENDIF}

Instead of directly using this unit in the unit tests, you use two (or more) different units that include that unit, e.g.:

unit UnitForTesting1;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;
function NotAlwaysAvaialble(): TSomeOtherType;

implementation

uses
  Other,
  Units;

implementation

{$DEFINE UNIT_TESTING}
{$DEFINE USE_EVERYTHING}
{$I 'ToTest'}

end.

and

unit UnitForTesting2;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;

implementation

uses
  Other,
  Units;

implementation

{$DEFINE UNIT_TESTING}
{$I 'ToTest'}

end.

I haven't tested this, but it should work.

On the other hand, this makes the testing code quite confusing and might lead to code not being tested as intended.

Share this post


Link to post
2 minutes ago, dummzeuch said:

You could use includes from different units for testing.

 

Lets say this is the unit you want to test:


{$IFNDEF UNIT_TESTING}
unit ToTest;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;
function NotAlwaysAvaialble(): TSomeOtherType;

implementation

uses
  Other,
  Units;

{$ENDIF}

function AlwaysAvailable(): TSomeType;
begin
  // implementation here
end;

{$IFDEF USE_EVERYTHING}
function NotAlwaysAvaialble(): TSomeOtherType;
begin
  // implementation here
end;
{$ENDIF}

{$IFNDEF UNIT_TESTING}
end.
{$ENDIF}

Instead of directly using this unit in the unit tests, you use two (or more) different units that include that unit, e.g.:


unit UnitForTesting1;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;
function NotAlwaysAvaialble(): TSomeOtherType;

implementation

uses
  Other,
  Units;

implementation

{$DEFINE UNIT_TESTING}
{$DEFINE USE_EVERYTHING}
{$I 'ToTest'}

end.

and


unit UnitForTesting2;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;

implementation

uses
  Other,
  Units;

implementation

{$DEFINE UNIT_TESTING}
{$I 'ToTest'}

end.

I haven't tested this, but it should work.

On the other hand, this makes the testing code quite confusing and might lead to code not being tested as intended.

I feel $INCLUDEing full units in an implementation section might not work, but I think I get the idea. And I’d agree with you that this would make it trickier than it should as the unit should have been torn into pieces for $INCLUDEing, all that only to test it.

Share this post


Link to post

I like to separate different aspects of stuff in completely different units, which where combined then in a wrapper unit, like so

unit DecideWhichUnitInPlace;

interface

uses
// This is the only place where separation takes place 
{$IFDEF UNIT_TESTING}
  UnitWithTests,
{$ELSE              }
  UnitWithoutTests
{$ENDIF             }
  ;

//Even separate classes and interfaces like that, not only functions
type
// This can point to completely different, but cleanly separated versions, depending on defines
{$IFDEF UNIT_TESTING}
    TMyType = UnitWithTests.TMyType;
{$ELSE              }
    TMyType = UnitWithoutTests.TMyType;
{$ENDIF             }
    ;



implementation

Then no defines are needed in those units at all

unit UnitWithTest;

interface

uses
  Whateever,
  isNecesssary;

function AlwaysAvailable(): TSomeType;
function NotAlwaysAvaialble(): TSomeOtherType;

type
  TMyType = class
    function AlwaysAvailable(): TSomeType;
    function NotAlwaysAvaialble(): TSomeOtherType;  
  end

implementation

uses
  Other,
  Units;

function AlwaysAvailable(): TSomeType;
begin
  // implementation here
end;

function NotAlwaysAvaialble(): TSomeOtherType;
begin
  // implementation here
end;


function TMyType.AlwaysAvailable(): TSomeType;
begin
  // call
  AlwaysAvailable;
end;

function TMyType.NotAlwaysAvaialble(): TSomeOtherType;
begin
  // call
  NotAlwaysAvaialble;
end;

end.

This is not the beest example to explain this and it depends highly on what you really want to achieve.
Maybe it gives you some helpful thoughts.

 

unit UnitWithoutTest;

interface

uses
  Whateever;

function AlwaysAvailable(): TSomeType;
//function NotAlwaysAvaialble(): TSomeOtherType;

type
  TMyType = class
    function AlwaysAvailable(): TSomeType;
    function NotAlwaysAvaialble(): TSomeOtherType;  
  end

implementation

uses
  Other,
  Units;

function AlwaysAvailable(): TSomeType;
begin
  // implementation here
end;

//function NotAlwaysAvaialble(): TSomeOtherType;
//begin
//  // NO implementation here, or completely removed
//end;


function TMyType.AlwaysAvailable(): TSomeType;
begin
  // call
  AlwaysAvailable;
end;

function TMyType.NotAlwaysAvaialble(): TSomeOtherType;
begin
  // call, ignore or mock, when function is NOT available
  //NotAlwaysAvaialble;
end;

 

Share this post


Link to post
14 hours ago, Dmitry Onoshko said:

I feel $INCLUDEing full units in an implementation section might not work, but I think I get the idea.

Including a full unit will definitely work, as long as it contains ifdefs that hide parts of it from the compiler. I have used this trick for many years to create pseudo templates long before Delphi had generics. The linked blog post explains it in depth.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×