Question

I trying to implement some basic automated testing on a 10 million LOC project that don't follow good OO pratices (ex: isolating business logic into classes/units) with the DUnit that comes along with Delphi 2010. I can't do normal unit testing on this project since each part of business logic is spread across a dozens of interdependent units, these 'groups' of units are, however, centered around certain 'main business logic screens' (ex: all invoice logic related units are centered on the main invoice screen), and since those screens are classes I can do 'main business logic screen class testing' instead of unit testing, but those 'main screens' still need a lot of stuff that is created during the process startup.

So I need to both:

  • Be able to run the bad project's startup stuff
  • Be able to access its objects

The bad project already have some exported functions that return pointers that I can cast to access it's objects, but I'm unable to call them either way:

If I create the bad project as a child process of the test process, the startup code run fine, but I can't find a way to call the exported functions without complex IPC methods or substantial change on the bad project's structure.

If I load the bad project's .exe as an dll as with the LoadLibrary function, calling any function exported by bad project's result in access violation and/or segfault errors, even this simple procedure:

procedure Test; {safecall;} {stdcall;}
begin
  showmessage('Yay!');
end;

How can I do both?

Was it helpful?

Solution

The approach you're talking about (using exported functions) is not going to fly. The simplest form of communication between two Win32 programs is to have them use SendMessage or PostMessage to talk to each other. Locating the window handle (usually by window class name) is step 1, sending a message is step 2.

Secondly, DUnit gets you nowhere near your goal, and TTestCase cannot be extended neatly to be a GUI Controller as that's not what it's for. It's for unit testing. Round peg, square hole. Write TTestCases for classes you can hive off and test, and use DUnit to provide test coverage for those parts of your system that you can provide test coverage for.

For UI testing, use a completely separate framework. There are two basic approaches done by Delphi programmers for automated integration tests of the sort you're proposing.

  1. A custom hack job. Such is what you are describing. Inside Embarcadero, there exists a framework which is called Zombie, something Nick blogged about back in 2007. Its approach is based on several kinds of "primitive IPC", usually involving a Win32 SendMessage or PostMessage window message from outside the program to a window handle of a control inside the program. However, the internal code IS SIGNIFICANTLY MODIFIED to permit zombie testing. No you can't have Teh Codez, they're internal and proprietary to Embarcadero. But it does illustrate that the approach does work, and does not require rewriting the whole application or writing a huge number of mock-classes, like unit testing would have done. If you want to go down the hack route, you will be writing your own User Interface Testing Framework, which should probably be completely separate from and use no DUnit code. You are welcome to try, but I'm telling you, it's a serious impedance mismatch. If I was starting my own custom framework in 2013, it would be DCOM based, because Delphi DCOM server code could be simply conditionally compiled into many programs, and DCOM would handle the function call "marshalling" details for you. I suspect I would get a year into the project, and I would give up, because in the end, I doubt I could make any system (DCOM or Win32 message based) pay off.

  2. A complete external testing tool which you write test scripts in, like AutomatedQA/SmartBear TestComplete. Your tests would not be compiled into a delphi test program, but run inside TestComplete, and Pascal-like script syntax is just one of the available options for writing your test scripts.

OTHER TIPS

We have had the same problem here. It looks like you really need a delphi library project (*.dll) for the export functions to work, (I suspect no initalization of the framework takes place when calling the function directly on an executable, no warranties).

NOTE: We are still using Delphi 5, so no dunit intergration here.

The solution we've used is to adding the dunit sources to our project (the .exe project) with a conditional DEFINE, and use this conditional define in the startup unit.

Sample startup code from our application:

  if ComServer.StartMode <> smAutomation then
  begin
    OurApplication.Login ;
  end;
{$IFDEF _AS_TESTRUNNER_}
    GUITestRunner.RunRegisteredTests;
{$ELSE}

  if OurApplication.HasStartCommands then
  begin
    Application.ShowMainForm := False ;
  end
  else begin
    if ComServer.StartMode = smAutomation then
      Application.ShowMainForm := False
  end;

  Application.Run;
{$ENDIF}

  OurApplication.Finalize;

When I use the _AS_TESTRUNNER_ conditional define, I must login first so our app (and db connections) get initialised. Followed bij the GUITestrunner of DUnit.

Testcases can be registered in the initialization part exactly as in the examples.

Works like a charm.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top